Docs

Measure AI usage and merged work in your repository.

TIER (Token Impact & Efficiency Ratio) runs on your computer or server. It is Apache-2.0 software with no licence fee; you pay for your AI tools and any hosting. Path A shows local Claude Code costs. Path B previews the dashboard with synthetic data. Path C combines laptop usage with merged pull requests for a team score, including opt-in Codex or Opencode usage. Path C reports teams only after a full calendar month closes and its grace period ends (14 days by default); a new setup may wait longer than a month. Raw session files stay on laptops; extracted usage goes to the server you choose, and an HTTPS provider can process data in transit. Start at S1 if you are new to terminals.

Already have a supported Go version (S2)? From a project folder with local Claude Code history, run this command for the cost table. The dot means the current folder; there is no prompt character to copy. This pins tag v0.6.1. The command uses GOBIN if set, otherwise bin under the first GOPATH entry, and runs the freshly installed binary by its full path. S2 explains installation and PATH.

tier_bin=$(go env GOBIN) && tier_gopath=$(go env GOPATH) && tier_bin=${tier_bin:-${tier_gopath%%:*}/bin} && go install github.com/tiermetric/tier/cmd/tierd@v0.6.1 && "$tier_bin/tierd" score --repo .

Quick start

Choose a cost report, synthetic preview or team pilot

Path A introduces local costs; Path B is a synthetic preview. S3 explains GitHub tokens for Path C. Path C needs an administrator; C2 needs someone who can run a web server. Each step says what to type, what you should see, and what to do if you don't.

Which computers. These instructions cover Mac and Linux, with bash or zsh. The Linux service and HTTPS recipes use Ubuntu with systemd; server users open a terminal through their existing SSH connection. This guide has no tested OS-version, memory or disk minimum; allow space for retained usage and webhook data. Release archives cover the platforms listed in S2; the -windows-amd64.tar.gz archive contains tierd.exe, but Windows is untested and unsupported, and owner-only database permissions do not hold there. There are no Windows steps here. Check command -v openssl curl nano: each installed tool prints a path; a missing one prints nothing. This check uses the terminal from S1; on Ubuntu install missing tools with sudo apt install openssl curl nano cron. sudo needs administrator rights. Have your administrator supply the tools on other systems.

Version and verification. This guide describes tag v0.6.1. Both installation routes use it. Before upgrading from v0.6.0, back up your database with tierd backup: the first writable open upgrades schema 10 through 11 to 12 even with corrective scoring off. Backup runs no migrations with either binary; check that the reported backup size is not about 4096 bytes. Older binaries refuse the upgraded database; rollback requires restoring the pre-upgrade backup and loses later recorded data. See the release changelog for the backup and restore steps. We checked these steps against the code. Earlier setup notes cover an Ubuntu pre-release run; the GitHub installation routes, Mac steps and Cloudflare dashboard clicks have not been run end to end.

A few words this page uses. A repository (or repo) is a project tracked with Git. A branch is a separate line of work. On GitHub, a pull request (PR) proposes changes; merging adds them to the project. An issue is a numbered task or bug report. Claude Code, Codex and Opencode are AI coding tools; TIER reads supported local usage records that are still present. An AI token is a piece of text counted for pricing. A GitHub or TIER token in S3 is instead a secret credential.

What the number means. TIER adds each merge’s size weight times its quality multiplier, divides by all captured AI cost in the period, including usage with no issue or merged result, then scales to points per $1,000 in US dollars. List prices put usage on a common basis; your invoice and the value of the work are separate. A higher number means more weighted merge points per list-price dollar. A month of hard debugging can score lower than a month of new features, whoever did the work. This is where the miles-per-gallon picture breaks: a mile is always a mile, and merged changes differ. Compare your own history with similar work, the same pricing and rubric, and documented capture limitations. There is no good-or-bad band. TIER grades the work, not the worker. TIER is built for coaching, not for evaluating developers. Per-person figures are not accurate enough to judge an individual.

Launch scope. Use TIER alone, or with people you fully trust. Everyone with a read, upload or administrator credential, or control of the database, must be trusted with all retained developer data and to report usage honestly. Use name-free team/division reports for shared use. Promotion of non-synthetic developer mode awaits controls for who can see named records, a viewing audit log and retention limits (#826). v0.6.1 refuses tierd serve --aggregation developer on real data, including loopback and read-only operation (#1253). Per-developer views and the per-issue report are not available on real data in v0.6.1 (#826). The synthetic demo (tierd demo), which generates its own invented data in a private temporary database and cannot load yours, shows these views read-only with capture and write routes absent. Upload credentials are not yet tied to a contributor's identity: a holder can submit usage as someone else, add fabricated contributors to the team list to meet k (the minimum number of contributors a report needs), or claim another uploader's event key first. That minimum does not authenticate contributors. These trust requirements are not enforced access controls. In v0.6.1, an ingest token permits only event uploads; admin credentials retain broader access. Corrective-work accounting (Event 3) is off by default and requires operator opt-in in v0.6.1. When enabled, only declared Corrects: #N relations count: eligible same-author pairs count provisionally before operator review; cross-author pairs require confirmation. A declared correction can lower the original’s points; undeclared repairs are not detected.

How merges earn points, and what the quality signals miss

Size labels set merge weights; without one, TIER uses changed lines and files, including generated and vendored changes. These are scoring conventions rather than measured business value. Read the rubric for weights, thresholds, label precedence and overrides.

Quality starts at full credit unless supplied through the outcomes API. A completed GitHub Actions failure on the default branch can match the exact recorded merge SHA within the 48-hour upper cutoff. v0.6.1 changes quality only for push-triggered runs; scheduled and dispatched runs remain audit-only, and merge-queue runs are dropped because their ref is not the default branch. Legacy CI effects remain labelled “CI trigger unknown” until operator-approved recovery. Workflow scope and pre-merge occurrence timestamps are not filtered, so environmental failures can still impose the CI floor. Other PR-head SHAs are not scored. A repository that gates locally gets no CI evidence from those checks. A strictly later attempt of the same workflow succeeding within 30 minutes can neutralise an already recorded failure; delivery order matters.

TIER does not measure the fraction reversed. A partial reversal that matches its supported revert rule receives the same message-selected floor as a full reversal—0.1 or 0.8. Other partial reversals can remain unobserved. Footer matching identifies recorded merge or squash SHAs, not constituent-commit ancestry. Missing or mixed keywords select 0.1; that classification does not establish a defect.

The floor values (0.7, 0.8, 0.1) are scoring conventions chosen by the project, not measured fractions of correctness, business value or value lost; no calibration study is published.

Matched quality evidence does not establish continuous webhook coverage. Coverage remains unknown unless a separately identified reconciliation establishes the supported event inventory for that repository and interval.

Backfill reconstructs merged outcomes, not their historical CI or revert signals. Those outcomes start at multiplier 1.0, which means no degrading signal was recorded. Later qualifying matched evidence can change live values; sealed reports remain unchanged.

In v0.5.2, a merged revert PR whose branch or body resolves an issue reference could earn a separate size-weighted outcome for the revert PR's author. The parser could treat an eligible number in GitHub's revert-<PR number>-… branch as an issue number. A separately recognised revert push could reduce the matched original outcome within the supported window; that did not necessarily reduce the team's total and could leave it higher. The reverter was not penalised merely for reverting.

v0.6.1. A recognised revert PR earns no positive credit. Reducing the original outcome still depends on a separately recognised revert push that matches that outcome within the supported window. Reverting a revert does not automatically restore credit.

A webhook for the team pilot. Follow C2 to collect live merges and supported quality signals in team mode. Team reports require the roster and contributor floor in C1 and a sealed calendar month; sealed months keep their stored quality even when a later qualifying revert arrives. Per-developer views and the per-issue report are not available on real data in v0.6.1 (#826).

Want the whole method? Read the TIER white paper →

A hiker at a trail map: A to “Your own usage”, B to “One project’s score”, C uphill to a TIER cabin, “Your team”. Caption: “Pick the trail that fits. You can always go further later.”

The trail illustration predates v0.6.1: Path B now shows invented demo data; real-data scores use Path C’s team pilot.

Shared steps

Before you start

Or let your agent do it

Agent prompts will appear here once their release checks pass. Use the manual steps below.

S1. Open a terminal

A terminal is a window where you type a command, press Enter, and read its answer as text. Browser and settings steps say where to go instead. The $, % or # at a terminal prompt is not part of a command.

  • Mac: press ⌘ Space, type Terminal, press Enter.
  • Linux: press Ctrl+Alt+T, or find "Terminal" in your applications menu.

For terminal commands, type each line and press Enter. Blocks introduced as file contents belong in the named text editor, not at the command prompt. To paste: ⌘ V on a Mac, Ctrl+Shift+V on Linux.

Example values. Some lines contain examples you must swap for your own: anything in angle brackets, such as <server-ip> or <your-server>; the names alex and alex-codes; and paths such as /Users/alex/Projects/my-app. Replace them and leave out the angle brackets: if the server's address is 192.168.1.42, type 192.168.1.42 where the line says <server-ip>, keeping the http:// before it and the :8080 after it. Copy everything else, including quotes, exactly.

You should see a line ending in $, % or #, waiting for you.

S2. Install TIER

Two ways. Either one gives you the same program, tierd.

Way 1: with Go. Go is the programming language TIER is written in. Installing it gives you a go command that downloads and builds TIER for you.

  1. Step 1. On a Mac, go to go.dev/dl, download the installer for your computer and run it. TIER needs Go 1.27.2 or later. On Linux there is no installer: Go comes as a compressed folder that you unpack and add to PATH yourself, as described at go.dev/doc/install. To install without Go, skip Way 1 and use Way 2 below, which needs no Go at all.

  2. Step 2. Open a new terminal and type go version.

    You should see a line such as go version go1.27.2 darwin/arm64. This guide uses 1.27.2, the minimum version; see the source README.

    If it says command not found: close the terminal, open a new one, try again. The installer changes settings that only new windows pick up.

    If your version fails the requirement above: update Go using the Mac installer at go.dev/dl, or the Linux archive instructions at go.dev/doc/install, then open a new terminal. The downloaded TIER executable in Way 2 needs no Go installation.

  3. Step 3. Type:

    go install github.com/tiermetric/tier/cmd/tierd@v0.6.1

    It may print some lines starting go: downloading and take a while. It worked if it ends with no error message and the prompt comes back.

  4. Step 4. Type tierd version.

    You should see output like this example: tierd … go1.27.2 darwin/arm64. A source install may show a commit-derived version rather than the release tag; the final part identifies your platform.

    If it says command not found: first check the spelling, and that step 3 ended without an error. If both are fine, find its installation directory: go env GOBIN prints an override if set; otherwise use the bin folder under the first entry of go env GOPATH. The usual directory is ~/go/bin. Use the actual directory in place of $HOME/go/bin in these lines, which add it to PATH:

    • Mac: echo 'export PATH="$HOME/go/bin:$PATH"' >> ~/.zshrc, then open a new terminal.
    • Linux: echo 'export PATH="$HOME/go/bin:$PATH"' >> ~/.bashrc, then open a new terminal.
    • These two lines assume the usual shells, zsh on a Mac and bash on Linux. echo "$SHELL" prints yours; if it ends in bash on a Mac, use ~/.bash_profile instead. For another shell, have its administrator add the directory to that shell’s PATH.
Explain this step: ~, $HOME and PATH

~ and $HOME both mean your home folder, the one with your Documents and Downloads in it. Use ~ when you are typing a path by itself and $HOME inside a longer value such as @$HOME/.tier/github-token; after the @, ~ is not expanded.

PATH is the list of folders your terminal searches when you type a program's name. command not found means the terminal could not find a program by that name in any of those folders: the name is misspelt, the install did not finish, or the program is in a folder not on the list. The echo … >> ~/.zshrc line adds the folder to the list for every future terminal window. Later on this page, a few places need the program's full path instead of its name; command -v tierd prints it, for example /Users/alex/go/bin/tierd.

Way 2: download the finished program. No Go is needed. Choose v0.6.1 to follow this guide; if its assets are not published, use the pinned source install in Way 1. Do not substitute an older release for the sealing steps.

  1. Step 1. Open github.com/tiermetric/tier/releases. Find the release tagged v0.6.1. Under its Assets, click the one file whose name ends in your computer's ending:

    • Mac with Apple silicon (M1 or later): -darwin-arm64.tar.gz
    • Mac with an Intel chip: -darwin-amd64.tar.gz
    • Linux: -linux-amd64.tar.gz (or -linux-arm64.tar.gz on a Raspberry Pi or ARM server)

    Not sure which Mac? Apple menu → About This Mac: "Chip: Apple M…" is arm64, "Processor: Intel" is amd64. Not sure which Linux? Type uname -m: x86_64 means amd64, aarch64 means arm64. Download the matching .tar.gz.sha256 file too. Your browser saves both in Downloads.

    If you have GitHub’s command-line tool, gh, download both files in a terminal: cd ~/Downloads && gh release download v0.6.1 --repo tiermetric/tier --pattern '*darwin-arm64.tar.gz*'. Replace darwin-arm64 with your platform ending from the list above.

  2. Step 2. Verify before unpacking. In Downloads, on Mac run shasum -a 256 -c tierd-v0.6.1-darwin-arm64.tar.gz.sha256; on Linux use sha256sum -c with your archive’s .sha256 filename. Change the platform ending to yours. The result must say OK; stop and download both files again if it differs. A checksum detects corruption, but a checksum from the same source alone does not prove origin. The release workflow also attests archives: with gh, verify gh attestation verify tierd-v0.6.1-darwin-arm64.tar.gz --repo tiermetric/tier. See Security for the release trust chain.

    The file is a compressed folder. In a terminal, type these two lines. If your ending is not darwin-arm64, change it in the second line, and in the line in step 3.

    cd ~/Downloads
    tar -xzf tierd-v0.6.1-darwin-arm64.tar.gz

    The commands name this release exactly, so they do not match other versions. If ~/bin/tierd already exists, moving the new file there replaces it; stop a running TIER server before replacing its binary.

    tar prints nothing when it works. You should see, when you type ls, a new folder named like the file without .tar.gz, containing one file, tierd.

    If it says no matches found or No such file or directory: the file is not in Downloads, or its ending is not the one in the line. Type ls to see what is there, and check the ending.

  3. Step 3. Move it somewhere permanent, and tell your terminal where:

    mkdir -p ~/bin && mv tierd-v0.6.1-darwin-arm64/tierd ~/bin/tierd

    then add ~/bin to your PATH as in Way 1 step 4, with $HOME/bin in place of $HOME/go/bin.

  4. Step 4. Type tierd version.

    You should see output like this example: tierd v0.6.1 go1.27.2 darwin/arm64: the version you downloaded, then your platform at the end.

    If a Mac refuses to run it with a security warning: first finish the verification above and confirm the download came from the TIER release. The archives are not a macOS notarized installer. If you accept running it, System Settings → Privacy & Security → “Open Anyway” is the platform override; xattr -d com.apple.quarantine ~/bin/tierd removes the download marker. Either bypasses that macOS check; gh is not a substitute for verifying the archive.

Explain this step: archive names

The filename contains the release and platform: tierd-v0.6.1-darwin-arm64.tar.gz is the Apple silicon build. Replace the platform ending with yours in every archive command. tar -xzf extracts the compressed archive; gh release download v0.6.1 selects that tag, and --pattern selects its files.

Optional demo, after either installation route. Type tierd demo. The terminal prints a banner saying the data is synthetic and a line Dashboard: http://127.0.0.1:8080/, then stays busy. Open http://127.0.0.1:8080 in your browser's address bar (the field showing the current web address) and look around: developers named demo-*, issues named DEMO-*, none of it yours. Back in the terminal, hold Ctrl and tap C. You should see tierd stopped. Do this before starting your own server later. The 8080 in the address is a port, a numbered door into the computer. Only one program at a time can answer at a port, so two cannot share 8080.

The demo uses tier-demo.db in the operating system’s temporary directory and recreates its synthetic data each run; your real database defaults to ~/.tier/tier.db. If 8080 is busy, use tierd demo --addr 127.0.0.1:8123 and open http://127.0.0.1:8123 instead.

S3. Save a token in a file

Several steps need a password-like value (a "token") saved in a file that only you can read. TIER then reads the file, so the token stays out of the commands you type and out of your command history, and it does not show on screen while you paste it. The recipe is the same every time; only the file name changes.

Which token, and where it comes from. The GitHub token comes from GitHub. The server operator generates the TIER admin token, ingest token, viewer token and webhook secret. The admin token stays with the operator; laptop shippers receive the ingest token and report readers receive the viewer token. Cloudflare supplies its own connector token for C2.

The GitHub token: made on github.com. Only backfill needs it, including for public repositories. This command uses github.com’s API; it has no GitHub Enterprise host option. Make a token limited to reading your project’s pull requests:

  1. Step 1. Signed in to GitHub, open github.com/settings/personal-access-tokens/new.
  2. Step 2. Give it a name such as tier and pick an expiry. When it expires, backfill stops with a 401 and you make a new one.
  3. Step 3. Under Resource owner, choose the account or organization that owns the project: the first word after github.com/ in the project's web address. If the organization you need is not in the list, ask its administrator. If GitHub shows a box asking why, write something like "Read merged pull requests for TIER".
  4. Step 4. Under Repository access, choose Only select repositories and pick your project.
  5. Step 5. Under Permissions → Repository permissions, set Pull requests to Read-only. GitHub adds Metadata: Read-only by itself. Leave everything else alone. Click Generate token.
  6. Step 6. Keep this page open while you prepare the terminal with the saving recipe below. Then copy the value, which starts with github_pat_. GitHub shows it only once; if you leave without saving it, revoke that token in the fine-grained token list and generate a replacement.
  7. Step 7. Save it with the recipe below, into ~/.tier/github-token.
  8. Step 8. Check GitHub Settings → Developer settings → Personal access tokens → Fine-grained tokens. If the token is marked pending, an organization administrator has to approve it before backfill will work.

The TIER admin token, ingest token, viewer token and webhook secret: generated on the server. openssl rand -hex 32 generates 32 random bytes as 64 hexadecimal characters. C1 step 2 saves the admin, ingest and viewer tokens; C2 step 1 saves the webhook secret. Generating saves directly to a private file. Keep the admin token with the operator, distribute the ingest token to laptop shippers and the viewer token to report readers, and paste the webhook secret into GitHub. Teammates use the saving recipe below for the ingest token or viewer token they receive.

Now save it. These three steps put a token into a file:

  1. Step 1. In a terminal, type this line and press Enter. Do this before you copy the token: copying this line would replace the token on your clipboard. It is set up for a GitHub token; for another token, change github-token to the file name the step asks for, in both places in the line and in the ls -l check below.

    bash -c 'mkdir -p "$HOME/.tier" && chmod 700 "$HOME/.tier" && read -rs tier_secret && (umask 077; printf "%s" "$tier_secret" > "$HOME/.tier/github-token") && chmod 600 "$HOME/.tier/github-token"'
  2. Step 2. The terminal goes quiet and waits. Now copy the token to your clipboard.

  3. Step 3. Paste the token into the terminal. Nothing appears on screen; that is on purpose. Press Enter.

You should see no output or error when the command finishes. ls -l ~/.tier/github-token shows a file whose permissions start -rw-------.

If it didn’t work: secret file … is empty means the paste did not take; repeat the three saving steps. This command invokes bash even from zsh. If bash is missing, have your administrator install it.

Replacing secrets. Save a replacement GitHub token over ~/.tier/github-token, retry backfill, then revoke the old token in GitHub. For TIER secrets, use C1 step 2’s replacement instructions. Cloudflare manages its connector token separately in its dashboard.

Explain this step: what the line does

mkdir -p ~/.tier makes a hidden folder called .tier in your home folder, if it is not already there; chmod 700 ~/.tier does the same for a folder made earlier. read -rs tier_secret waits for one line of typing without echoing it. (umask 077; printf "%s" "$tier_secret" > file) writes exactly that text into the file, and the umask 077 in front makes a new file private from the moment it exists, rather than readable by others for an instant. chmod 600 makes the file readable and writable by you alone even if it was there before: writing over an existing file keeps its old permissions.

From then on, wherever a command wants the token, give it the file with an @ in front: --token @$HOME/.tier/github-token. The @ tells TIER "open this file and use what is inside". Only tierd understands the @ form; curl does not.

Path A

Path A: look at your own AI usage, on your computer

Who it's for. One developer who wants to see where the AI money went. It reads the records already on that computer.

What you need. TIER installed (S2). A project folder on this computer that you have used Claude Code in. That is all. If you have never used Claude Code, this path has nothing to read; Codex and Opencode usage shows up in paths B and C.

Steps.

  1. Step 1. Open a terminal (S1) and go into the project folder. Type cd, a space, and the folder's path in quotes:

    cd "/Users/alex/Projects/my-app"

    Replace the path with yours. Then type pwd; it prints the full path of where you are. Keep it; later steps ask for it.

    Explain this step: finding the folder's path

    Type cd (with the space), then drag the project folder from Finder or your file manager into the terminal window. The path pastes itself. Press Enter. The quotes matter if the path has spaces in it.

    You should see the prompt come back with the folder's name in it.

    If it says No such file or directory: the path is wrong; drag the folder in as above.

  2. Step 2. Run:

    tierd score --repo .

    The . means "the folder I am in right now".

    Explain this step: what it reads

    TIER looks for retained Claude Code session records in a hidden folder, .claude/projects, inside your home folder. Supported records contain a starting project folder, branch and usage counters; missing or deleted records cannot be recovered by this command. Here a token means something different from S3: it is a small piece of text the AI reads or writes, and it is how AI use is counted and priced. tierd score reads the records for this project, prices them from a built-in table of published list prices, and prints the total. This local score path makes no upload or GitHub request. Its default is the last 90 days; to reach further back, add --since and a date before your oldest session, for example --since 2026-01-01.

    You should see a line naming the price table (for example, price table: embedded default (version 14, …)), a table with one row per developer and a Cost ($) column, then a Cost by Issue table and the issue-attributed share of Claude Code spend, then Tip: run tierd serve to record PR outcomes and compute full TIER scores. The attributed share has no outcome join and cannot evaluate server ranking’s outcome requirements. That tip on its own will not give you a score; a real-data team score requires Path C’s contributor floor and sealed calendar month. The local tierd score cost table shows costs by developer and issue on your own laptop; it is separate from the server’s per-developer views and per-issue report, which are unavailable on real data in v0.6.1 (#826).

    If it says No Claude Code sessions found for the given repo and time period.: one of three things. You are in the wrong folder (check pwd); you never used Claude Code in this project on this computer; or the sessions are older than 90 days (add --since with an earlier date, as above). For anything else, run:

    tierd doctor --repo .

    It prints a checklist of [OK ], [WARN] and [FAIL] lines, including where it looked for the records and how many it parsed.

  3. Step 3. Read the result. Rows named issue-42 are usage tied to a task. Rows starting unattributed: are usage TIER could not tie to one, for example because the recorded branch had no issue number. Usage on main gets its own row, unattributed: exploratory (main). TIER calls it exploratory only because it is not tied to an issue; it does not know what you actually did there.

    Explain this step: putting the issue number in the branch name

    This is optional for path A. On your project's GitHub page, open Issues and pick the task you are about to work on; its number is the #42 after the title. Use the number without the #; 42 here is only an example. Before you start work, in a terminal inside the project folder, make a branch named with a word, a slash, the number, a dash and a short description: git checkout -b feature/42-login-button. Do your Claude Code work on that branch. When you open the pull request, write closes #42 in its description. Start the AI session inside this branch’s checkout. Old sessions do not change; new ones land under issue-42.

Prices. v0.6.1 embeds table version 14, effective 2026-10-12. Unknown models use a warned estimate; missing counters cannot be recovered from a bill. The price reference covers sources, input/output/cache rates, overrides, registered table identities and repricing. Repricing can change an unsealed score.

Advantages. The command reads local usage records without uploading them.

Disadvantages. This is the cost half only; it knows nothing about what got merged, so it is not a TIER score. The cost is at published list prices and can differ from your bill. It reads Claude Code's records only.

Path B

Path B: preview the dashboard with invented data

Per-developer views and the per-issue report are not available on real data in v0.6.1 (#826). The synthetic demo (tierd demo), which generates its own invented data in a private temporary database and cannot load yours, shows these views read-only with capture and write routes absent.

v0.6.1 refuses tierd serve --aggregation developer on real data, including loopback and read-only operation, until #826’s named-viewer, audit and retention controls ship (#1253).

  1. Install TIER using S2, then run:

    tierd demo
  2. Open the printed dashboard address. The synthetic banner, demo-* developers and DEMO-* issues identify invented data. Press Ctrl+C in the terminal when finished.

For your own data, Path A gives a local Claude Code cost table. For a trusted-team pilot, use Path C: team mode, a roster, the k contributor floor and a sealed full UTC calendar month after its grace period. Team mode has no per-issue view.

Path C

Path C: a shared board for your team

Path C comes in two parts. C1 puts one board on your own network, with teammates sending their usage to it and merges refreshed by hand. C2 captures merges straight from GitHub, which needs a public HTTPS address and someone comfortable running a web server. Both show sealed calendar months: reports stored once after the month closes and served unchanged, rather than a live team score. Do C1 first; C2 is optional.

C1. A team board on your own network

Who it's for. A team lead, working with whoever looks after your computers.

What you need.

  • A Mac or Linux computer that you can sit at and sign in to, on the same private network (office or home) as your teammates' laptops, and that stays on. This page calls it "the server". TIER is installed on it (S2) exactly as on a laptop. Provisioning a rented or cloud server and SSH access is an administrator’s job. Once provisioned, it can use these Ubuntu service steps and C2’s HTTPS recipes; see the rented-server firewall note below.
  • A GitHub token that can read your project's pull requests. If you have not made one, make it as S3 shows. Then save it on the server itself, with the S3 recipe typed in a terminal on the server, into ~/.tier/github-token there. A file on your laptop is not on the server.
  • Three separate credentials, made in step 2: an admin token that stays with the operator, an ingest token for laptop shippers, and a read token for report viewers.
  • The server's firewall letting connections in on port 8080. On a Mac, macOS asks the first time and you click Allow. On Ubuntu Linux, type sudo ufw allow 8080/tcp on the server. It asks for that computer's password, which does not show as you type. If it says you are not allowed, whoever manages that computer has to do it.
  • On a rented or cloud server, do not open 8080 to the whole internet. Such a server usually has a second firewall in the provider's control panel (a "security group"). If you add an 8080 rule there, let in only your office's IP addresses. That keeps strangers out, but the password still crosses the internet unencrypted (see "A word on safety" below), so for a rented server C2's HTTPS front is the better answer.
  • A list of who is on which team, by GitHub login. Each reported figure needs k contributing people: cost needs people with captured spend, points need people with merged work. The default is five. Being on the roster alone fills no seat; with too few contributors, the board withholds numbers. The technical reference covers contributor exclusions, per-measure floors and the minimum setting.
  • Every teammate's usage. The board counts the merges of everyone in the project, but only the usage that laptops send in step 8. Until every contributing teammate is sending, their merges arrive with no cost against them.

Steps, on the server.

  1. Step 1. Install TIER (S2) and make the folder:

    mkdir -m 700 -p "$HOME/.tier" && chmod 700 "$HOME/.tier"
  2. Step 2. Make three different tokens. Run this once on the server:

    (umask 077; openssl rand -hex 32 > "$HOME/.tier/api-token")
    (umask 077; openssl rand -hex 32 > "$HOME/.tier/read-token")
    (umask 077; openssl rand -hex 32 > "$HOME/.tier/ingest-token")
    chmod 600 "$HOME/.tier/api-token" "$HOME/.tier/read-token" "$HOME/.tier/ingest-token"

    Running it again replaces the file; use the replacement instructions below.

    Who can read. The ingest-only token (--ingest-token) for laptop shippers is accepted only on POST /api/v1/events. It grants no read, outcome, export, erase or proxy access and is not tied to a developer identity. The admin token (--api-token) retains reads, writes and erasure; the viewer token (--read-token) is read-only and the metrics token permits scraping only. Admin holders can read every developer’s stored identifying records in any reporting mode and change aliases and the roster. k-anonymity limits what viewers of team and division reports see; it does not restrict admin token holders. Per-developer views and the per-issue report are not available on real data in v0.6.1 (#826). Keep the admin token with the operator, give the read token to report viewers, and give only the shared ingest token to laptop shippers. The server refuses equal tokens.

    Explain this step: distribute credentials

    Keep the admin token on the server. Transfer the ingest token to trusted laptop shippers and the read token to viewers through a password manager or in person. The security guide covers credential handling, metrics access, lockout settings and trusted proxies.

    Replacing TIER secrets. They have no built-in expiry. Replace the file, then restart a hand-run server with Ctrl+C and its full command, or restart the C1 service with C2 step 2’s service commands. Secrets are read at startup, with no old/new overlap. Keep the replacement admin token with the operator. Distribute a new ingest token to laptop shippers and a new viewer token to report viewers; replacing a webhook secret also requires updating GitHub’s Secret. Removing one teammate requires rotating the shared ingest token for remaining shippers, and the read token if that teammate held it.

    You should see cat ~/.tier/api-token print one line of 64 hexadecimal characters.

  3. Step 3. Save your GitHub token on the server (S3, into ~/.tier/github-token), then load the merged pull requests:

    tierd backfill --repo your-org/your-repo --token @$HOME/.tier/github-token

    Replace your-org/your-repo with the two components after github.com/ in your project’s address, for example your-org/your-repo.

    You should see backfill complete: … merged=N inserted=N …. Use the same recent --since date for backfill and ship; deleted local logs cannot be recovered by choosing an older date.

  4. Step 4. Start the server so other computers can reach it. First by hand, to check it works:

    tierd serve --aggregation team --addr 0.0.0.0:8080 --api-token @$HOME/.tier/api-token --read-token @$HOME/.tier/read-token --ingest-token @$HOME/.tier/ingest-token
    Explain this step: 0.0.0.0, and why the token is required

    127.0.0.1 means "only this computer". 0.0.0.0 means "every network this computer is on": the office Wi-Fi, or on a rented server, the internet. Because the board shows real spend, TIER will only listen on an open address when you also give it a password. Without --api-token it prints refusing to bind "0.0.0.0:8080" without an API token and stops.

    --aggregation team makes the board show team reports; admin routes retain identifying records as step 2 explains. For reporting’s use and privacy limits, read Legal & Privacy. Until step 6 loads the team list, TIER does not know who is on which team, so it will warn at startup that the team list is empty. In v0.6.1, team and division reports serve sealed calendar months only. Before step 9 arms sealing, score requests return 404 with sealing not armed. That’s expected until step 9.

    No --watch-repo here: the server has no laptop records of its own to watch. The laptops send theirs in step 8.

    You should see API auth scopes write_armed=true, aggregation mode … mode=team k_anonymity=5, a warning that org_hierarchy is empty, and tierd listening addr=0.0.0.0:8080. The terminal stays busy. write_armed=true means an admin token enabled authenticated write routes; it does not mean reporting is sealed.

    If it didn't work: the "refusing to bind" message means the --api-token flag is missing or the file is empty. "address already in use" means something else is on 8080.

    Once it starts cleanly, press Ctrl+C and set it up to run on its own, so it keeps going after you close the terminal. On Linux the setup below also keeps it running when you log out and starts it after a restart. On a Mac it runs only while you are logged in: it starts when you log in and stops when you log out, so keep that account logged in on the server (locking the screen is fine).

    Explain this step: keeping the server running on its own

    A server started in a terminal stops when that window closes. The operating system can run it for you instead and restart it if it crashes. Every path below must be the full path, with no ~: run command -v tierd to get the program's path, and echo "$HOME" to get your home folder's full path. Put that in place of /Users/alex or /home/alex in the examples.

    Keep all three token flags in the service. If you changed --k-anonymity by hand, add it here too. On a Mac each flag and its value go on one more line inside <array>, as a pair such as <string>--k-anonymity</string><string>3</string>; on Linux they go on the end of the ExecStart line.

    Mac: a launchd agent. The earlier setup notes did not test these Mac steps. The Linux alternative uses systemd. Make the log folder and open a new, empty file in nano, a simple text editor that runs inside the terminal:

    mkdir -p ~/.tier/logs ~/Library/LaunchAgents
    nano ~/Library/LaunchAgents/org.tiermetric.tierd.plist

    Paste the text below into it. Then replace /Users/alex everywhere with what echo "$HOME" printed, and replace /Users/alex/go/bin/tierd, the first line inside <array>, with what command -v tierd printed. Leave org.tiermetric.tierd as it is. Use the arrow keys to move around. Save and quit: press Ctrl+O, then Enter, then Ctrl+X. Use nano rather than TextEdit, which can save the file in a format launchd cannot read.

    <?xml version="1.0" encoding="UTF-8"?>
    <plist version="1.0">
    <dict>
      <key>Label</key><string>org.tiermetric.tierd</string>
      <key>ProgramArguments</key>
      <array>
        <string>/Users/alex/go/bin/tierd</string>
        <string>serve</string>
        <string>--db</string><string>/Users/alex/.tier/tier.db</string>
        <string>--aggregation</string><string>team</string>
        <string>--addr</string><string>0.0.0.0:8080</string>
        <string>--api-token</string><string>@/Users/alex/.tier/api-token</string>
        <string>--read-token</string><string>@/Users/alex/.tier/read-token</string>
        <string>--ingest-token</string><string>@/Users/alex/.tier/ingest-token</string>
      </array>
      <key>RunAtLoad</key><true/>
      <key>KeepAlive</key><true/>
      <key>ThrottleInterval</key><integer>30</integer>
      <key>StandardOutPath</key><string>/Users/alex/.tier/logs/tierd.out.log</string>
      <key>StandardErrorPath</key><string>/Users/alex/.tier/logs/tierd.err.log</string>
    </dict>
    </plist>

    RunAtLoad starts it when you log in (and it stops when you log out); KeepAlive restarts it if it stops; ThrottleInterval waits 30 seconds between restarts so a broken command can’t restart in a loop. Load it:

    launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/org.tiermetric.tierd.plist

    You should see nothing, and then launchctl list | grep tierd prints a line like 72994 0 org.tiermetric.tierd: a process number, a 0, and the label. The server's own startup lines are in ~/.tier/logs/tierd.err.log; cat ~/.tier/logs/tierd.err.log prints them. To stop it, or before editing the file (C2 step 2 adds a flag): launchctl bootout gui/$(id -u)/org.tiermetric.tierd, then bootstrap again after the edit.

    If launchctl list shows - instead of a number: it is not running; cat ~/.tier/logs/tierd.err.log prints its log. Possible causes are a wrong path on the first line inside <array>, a ~ somewhere in the file, or the by-hand server from above still holding port 8080.

    Linux: a systemd unit. Make the folder and open a new, empty file in the nano editor:

    mkdir -p ~/.config/systemd/user
    nano ~/.config/systemd/user/tierd.service

    Paste the text below, replace /home/alex with what echo "$HOME" printed and /home/alex/go/bin/tierd with what command -v tierd printed, then save and quit with Ctrl+O, Enter, Ctrl+X:

    [Unit]
    Description=TIER server
    After=network.target
    
    [Service]
    ExecStart=/home/alex/go/bin/tierd serve --db /home/alex/.tier/tier.db --aggregation team --addr 0.0.0.0:8080 --api-token @/home/alex/.tier/api-token --read-token @/home/alex/.tier/read-token --ingest-token @/home/alex/.tier/ingest-token
    Restart=always
    RestartSec=5
    
    [Install]
    WantedBy=default.target

    Then:

    systemctl --user daemon-reload
    systemctl --user enable --now tierd
    loginctl enable-linger "$USER"

    The last line keeps your user services running when you are not logged in. You should see systemctl --user status tierd report active (running); if that fills the screen, press q to get back. journalctl --user -u tierd -f shows its log as new lines arrive; press Ctrl+C to stop watching, which does not stop TIER.

  5. Step 5. Find the server's address on your network. On a Mac (not exercised in the earlier setup notes): ipconfig getifaddr en0 (try en1 if it prints nothing). On Linux: hostname -I. It looks like 192.168.1.42. This page calls it <server-ip>. Check from another computer on the same network: open http://<server-ip>:8080/api/v1/livez in a browser. If the command printed several addresses, try each one this way and keep the one that works. Give that address and the ingest token to laptop shippers, and the address and read token to report viewers. Keep the admin token with the operator. If it changes later, every laptop's --server address has to change too.

    You should see {"status":"alive",…}. That address needs no password on purpose.

    If the page never loads: the firewall (see "What you need"), or the wrong address.

  6. Step 6. Tell TIER who is on which team. In a terminal on the server, send the list. Each entry is a GitHub login and a team name you choose. This example puts five people in one team to match the default floor; each still needs captured activity for every nonzero measure. Replace the sample entries with your people: keep the keys developer, team, division and org, the quotes, and the brackets and braces; put a comma between entries and none after the last. A person's GitHub login is the part after github.com/ in their profile address. Choose your team, division and organization names and spell each consistently; --aggregation division uses the division field instead of the team field. Each person has one team and division at a time.

    printf 'Authorization: Bearer %s\n' "$(cat ~/.tier/api-token)" |
      curl -i -X POST http://127.0.0.1:8080/api/v1/org_hierarchy -H @- -H "Content-Type: application/json" -d '[{"developer":"alex-codes","team":"payments","division":"engineering","org":"your-org"},{"developer":"sam-b","team":"payments","division":"engineering","org":"your-org"},{"developer":"jo-l","team":"payments","division":"engineering","org":"your-org"},{"developer":"lee-m","team":"payments","division":"engineering","org":"your-org"},{"developer":"pat-r","team":"payments","division":"engineering","org":"your-org"}]'
    Explain this step: the team list

    printf pipes the credential to curl -H @-, keeping it out of curl’s process arguments. A bad entry rejects the whole list with 400; fix it and resend. Omitted people stay on the roster. A placement starts when the request reaches the server; it cannot be backdated. Later moves keep earlier team history. Record the day you ran this step for step 9. The hierarchy API contract covers moves, departures and invoice membership.

    You should see HTTP/1.1 201 Created and a body including "accepted":5; the number is how many entries you sent. printf 'Authorization: Bearer %s\n' "$(cat ~/.tier/api-token)" | curl -H @- http://127.0.0.1:8080/api/v1/org_hierarchy lists what is stored.

  7. Step 7. Join any teammate whose computer username differs from their GitHub login. Use the admin token on the server:

    printf 'Authorization: Bearer %s\n' "$(cat ~/.tier/api-token)" |
      curl -i -X POST http://127.0.0.1:8080/api/v1/developer_alias -H @- -H "Content-Type: application/json" -d '{"alias":"alex","canonical":"alex-codes"}'

    Ask each teammate to type whoami on their laptop and send you the result and their GitHub login. Run the alias command once for each person whose names differ. You should see HTTP/1.1 201 Created. If two people share an OS username, each must use --developer with their GitHub login in step 8. Remove a wrong shared alias and correct mixed records before sealing, using the identity and correction API. New uploads cannot split already mixed records.

Steps, on each teammate's laptop.

  1. Step 8. Install TIER (S2), save the ingest token, and send your usage. Get the ingest token from whoever runs the server, by a password manager or in person, and save it with the S3 recipe into ~/.tier/ingest-token. Then, using your own project folder's path:

    tierd ship --server http://<server-ip>:8080 --repo "/Users/alex/Projects/my-app" --api-token @$HOME/.tier/ingest-token

    Claude Code records are read by default. Add --codex-rollout, --opencode or --muse only for tools you use. Omitting a tool’s flag omits that tool’s usage; check the per-repo summary and exclusions. Use --developer with your GitHub login if step 7 requires it.

    You should see Per-repo summary:, your folder with counts above zero, then Shipped N events.

    If it didn't work: 401 means the token file's contents differ from the server's; save it again. "connection refused" means the wrong <server-ip>, the firewall, or the server is not running. sessions_with_events=0 can mean the wrong folder, no recent retained records, or a missing tool flag. Run tierd doctor --repo with your project’s quoted full path, and check the per-repository summaries, retained session files and collector flags.

    Repeat scans recover retained usage within --since (90 days by default), including after failed uploads; deleted or older records may be lost. For scheduled uploads, use cron on each laptop and let one run finish before the next. Check logs: there is no per-laptop completeness certificate.

    Explain this step: running ship every 15 minutes with cron
    1. Step 1. Find the program's full path: command -v tierd prints something like /Users/alex/go/bin/tierd. Cron runs with almost no PATH, so the short name tierd would fail there.
    2. Step 2. Type crontab -e. It opens a text editor in the terminal. On a Mac that is usually vi: press i to start typing, paste the line below, press Esc, then type :wq and Enter to save and quit. On Linux it is often nano: paste, then Ctrl+O, Enter, Ctrl+X.
    3. Step 3. The line, with your paths. If your ship command by hand needed --codex-rollout, --opencode or --developer, add them to this line too:
      */15 * * * * /Users/alex/go/bin/tierd ship --server http://<server-ip>:8080 --repo "/Users/alex/Projects/my-app" --api-token @/Users/alex/.tier/ingest-token >> /Users/alex/.tier/ship.log 2>&1

      */15 * * * * means "at every 15th minute, every hour, every day". Use full paths for the token file too.

    4. Step 4. Check it took: crontab -l prints your line. After its next run, tail -n 20 ~/.tier/ship.log should show the shipment result or its error. The redirect appends output to that laptop’s log; rotate or truncate it periodically. If runs overlap the next 15-minute mark, lengthen the schedule.

    You should expect the next upload at the next 15-minute mark, as long as the laptop is awake and can reach the server. Uploads do not change the sealed month on the board: new usage can appear only when its calendar month seals after closing and the default 14-day grace period. If macOS shows a box asking whether Terminal or cron may open files in a folder, click Allow. If nothing arrives: run the cron line above by hand in a terminal, leaving out the */15 * * * * at its start; check ~/.tier/ship.log for its output, or remove the redirect to see output in the terminal. If it works by hand but not on the timer, the problem is in the cron setup.

    Explain this step: what leaves your laptop

    Each upload contains token counts, model names, timestamps, session/message identifiers, repository and issue identifiers, and the developer name. The usage parsers copy an allowlist rather than prompt/reply or file contents. This applies to laptop usage uploads; webhook bodies and watcher checkpoints have different fields. The code and stored-field list are in Privacy.

Back on the server.

  1. Step 9. Arm sealing on the server, after backfill and the laptops’ uploads. Finish identities and the roster before sealing. Keep serve running so its source configuration stays registered; seal can share its database. In another terminal under the same account, preview with the settings used by the service:

    tierd seal --arm earliest --dry-run --db "$HOME/.tier/tier.db" --aggregation team

    The first readable month must be a full UTC calendar month of cost coverage in which the roster was already effective for at least k counted people. It becomes readable after it closes and passes the default 14-day grace, configured sources settle through its end, sealing succeeds and the contributor floors are met. The preview’s tierd seal: will seal … line shows the month and settings. Check the capture horizon, roster and aliases and confirm every laptop supplied retained usage throughout the month. The dry run writes nothing.

    Roster placement cannot be backdated: history before a person’s first assignment stays in other, and unrostered people do not count toward k. If a populated other falls short of a contributor floor, the whole report is withheld. --arm earliest selects by cost coverage; if that month predates the roster, choose a later eligible month by using tierd seal --arm YYYY-MM in both the preview and confirmation commands, keeping the other flags.

    If you customized the service, use its same database, aggregation, configuration, prices, k and grace settings for seal. The sealing reference covers flags, grace limits, configured-source coverage and stalls. When the preview is correct, run:

    tierd seal --arm earliest --db "$HOME/.tier/tier.db" --aggregation team

    The prompt asks whether history is backfilled, settings match serve and the printed month is correct. Type y or yes; Enter or Ctrl+C cancels. Success says armed: sealed … and pinned it as the earliest sealed month. The first seal pins the earliest published month permanently: earlier months stay unpublished and later cost, quality, aliases, prices or roster edits leave this stored month unchanged.

    With no eligible history, wait for a full covered month and its grace period or backfill available retained history. A refused preview leaves sealing unarmed; correct the named cause or wait. A new pilot may wait longer than a month.

    This C1 server receives laptop uploads, whose completeness you check yourself before sealing; grace alone gates their arrival. Servers with scheduled collectors or pollers also require recorded coverage as described in the sealing reference.

    tierd seal --status --db "$HOME/.tier/tier.db" --aggregation team --server http://127.0.0.1:8080 --api-token @$HOME/.tier/api-token

    Status prints the pin, latest month, next owed month and, with --server, any reported stall reason. Repeat customized settings here too. After arming, serve checks at startup and hourly. Select a published month with the dashboard’s Sealed month picker; the API contract covers month selection, comparisons and verification.

    Open the board. Everyone opens http://<server-ip>:8080.

    You should see the page load and a password box labelled API token beside Refresh. Paste the read-only viewer token and click Refresh. The browser keeps it while the tab is open. The board shows the newest sealed month; smaller teams and unassigned people fold into “other”, subject to the contributor rules above. Until a month is sealed there are no team numbers to show.

    If everything is in one row called “other”: check the roster and contributors for that sealed month. Later roster changes leave it fixed. If a populated “other” fails a contributor floor, the whole report is withheld. If no one is in “other”, it hides nothing.

    If the page keeps asking for the token: check for a stray space; the token is the 64 characters in the file. Repeated failures can temporarily block access. Behind Caddy or a tunnel, users can share a lockout; use the security guide in step 2 to configure it.

Keeping merges current in C1. On the server, refresh imported merges as follows. Stop the server: on a Mac (not exercised in the earlier setup notes) launchctl bootout gui/$(id -u)/org.tiermetric.tierd, on Linux systemctl --user stop tierd. Run step 3's backfill again. Start it again: on a Mac (not exercised in the earlier setup notes) launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/org.tiermetric.tierd.plist, on Linux systemctl --user start tierd. Laptops keep running step 8; attempts while the server is down fail, so check their logs and rescan after restart. backfill does not see reverts or failed tests. C2 removes that chore.

A word on safety. On this path the server speaks plain HTTP, so the ingest token, read token and usage travel unencrypted between laptop and server. Anyone on the network who captures a token gains its scope of access. Use this setup only if whoever is responsible for your network accepts that risk. Keep port 8080 closed to the public internet; that is what C2's HTTPS front is for.

Advantages. One board for the whole team. The team board shows group reports; credential access is explained in step 2. With the server on your own private network and 8080 closed to the internet, the administrator can restrict it to that network; verify the firewall and absence of public forwarding.

Disadvantages. A server to set up and keep running, an ingest token to distribute to laptop shippers, and a read token to distribute to report viewers. The admin token stays with the operator. Every contributing device and supported tool must send retained usage; unseen usage can leave the figures incomplete. Teammates run ship or set up the 15-minute timer. Small cohorts are hidden from team reports. Merges are still refreshed by hand. Plain HTTP.

C2. Live merges from GitHub

Who it's for. A team that has C1 working and wants merges captured automatically for the next eligible sealed month. This part needs someone comfortable running a web server.

A webhook courier carries a letter with a green “Signed” seal through an HTTPS gate to a TIER clerk, who checks it; a drawer holds the secret. Caption: “Signed at GitHub, checked at your door. The key never travels.”

What you need.

  • C1 done.
  • A public address with HTTPS, such as https://<your-server> (your server's public address, e.g. tier.example.com), that forwards to http://127.0.0.1:8080 on the server. GitHub's servers must be able to reach it from the internet. GitHub signs each message with the secret so TIER can tell it is genuine, and the messages carry pull-request titles, descriptions and commit messages, so the address must use HTTPS to keep them private on the way. Common ways are a reverse proxy such as Caddy or nginx holding the certificate, or a tunnel such as Cloudflare Tunnel. Step 3 covers two of them: Cloudflare Tunnel and Caddy.
  • For the Cloudflare Tunnel option, a Cloudflare account and a domain managed in Cloudflare.
  • A shared webhook secret: GitHub signs each payload with it, and TIER checks the signature before processing the message.
  • Admin rights on the GitHub repository, to add a webhook.

Webhook storage. TIER stores processed raw GitHub bodies, compressed, in the same database: PR/commit text can include names, email addresses and anything pasted into descriptions. The default retention is 90 days with a 50,000-row cap, pruned on database open and daily. Developer erasure does not remove these raw payloads. See Privacy for the full stored-field and retention account.

Steps.

  1. Step 1. Make the webhook secret, on the server:

    (umask 077; openssl rand -hex 32 > "$HOME/.tier/webhook-secret")
    chmod 600 "$HOME/.tier/webhook-secret"
    cat "$HOME/.tier/webhook-secret"

    You should see one line of 64 characters. Keep it on screen for step 4.

  2. Step 2. Restart the server with the secret. On a Mac, reopen the file with nano ~/Library/LaunchAgents/org.tiermetric.tierd.plist and, just above the </array> line, add <string>--webhook-secret</string><string>@/Users/alex/.tier/webhook-secret</string>. On Linux, reopen it with nano ~/.config/systemd/user/tierd.service and add --webhook-secret @/home/alex/.tier/webhook-secret to the end of the ExecStart line. Either way, replace /Users/alex or /home/alex with what echo "$HOME" prints, keep everything already there, and save with Ctrl+O, Enter, Ctrl+X. Then reload: on a Mac (not exercised in the earlier setup notes) launchctl bootout gui/$(id -u)/org.tiermetric.tierd and launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/org.tiermetric.tierd.plist; on Linux systemctl --user daemon-reload && systemctl --user restart tierd. By hand, the full line is:

    tierd serve --aggregation team --addr 0.0.0.0:8080 --api-token @$HOME/.tier/api-token --read-token @$HOME/.tier/read-token --ingest-token @$HOME/.tier/ingest-token --webhook-secret @$HOME/.tier/webhook-secret

    You should see the startup lines from C1, and no warning about TIER_WEBHOOK_SECRET.

    If you see TIER_WEBHOOK_SECRET is not set — POST /webhook/github is disabled: the flag is missing. Without a secret the webhook address does not exist at all, on purpose.

  3. Step 3. Pick one HTTPS option: Caddy (open ports 80/443, your own DNS) or Cloudflare Tunnel (no open ports, a domain managed in Cloudflare).

    Put HTTPS in front so that https://<your-server>/webhook/github reaches the server.

    Caddy on Ubuntu. These service and package commands target Ubuntu with systemd. Use a hostname you control in place of tier.example.com everywhere below; choose it in your domain’s DNS settings. Caddy needs a publicly reachable server and incoming ports 80 and 443. For an office/home server behind a router, the network administrator must supply a public address and forward both ports to the server; this may be unavailable behind carrier-grade NAT. The tunnel option needs no incoming forwarding. On a Mac, have your web-server administrator install and manage the HTTPS front; the Ubuntu commands below do not apply.

    Install Caddy, on the server. Ubuntu ships it in its own package archive:

    sudo apt update && sudo apt install -y caddy

    You should see Caddy's service running after the install; check it with sudo systemctl status caddy. Press q to get back.

    Keep TIER on this computer only. Open nano ~/.config/systemd/user/tierd.service and change --addr 0.0.0.0:8080 on the ExecStart line to --addr 127.0.0.1:8080. Keep the admin token, the webhook secret from step 2 and every other flag already there. Save and quit with Ctrl+O, Enter, Ctrl+X, then restart it:

    systemctl --user daemon-reload
    systemctl --user restart tierd

    For a Mac server, make the same loopback change in ~/Library/LaunchAgents/org.tiermetric.tierd.plist: replace the address value after --addr with 127.0.0.1:8080. Reload it with C2 step 2’s launchctl commands. Keep both secrets and all other flags. This applies to a tunnel too.

    Explain this step: why only Caddy faces the internet

    127.0.0.1 lets Caddy reach TIER on the same server, but keeps other computers from connecting to TIER directly. Caddy receives HTTPS on port 443, holds the certificate and forwards the request to 127.0.0.1:8080. The admin token and webhook secret still protect TIER's requests.

    Point the hostname at the server. In your DNS provider's control panel, set the A record for tier.example.com to the server's public IPv4 address, and, if it has IPv6, its AAAA record to the server's public IPv6 address. Keep these records DNS-only, not proxied.

    Open ports 80 and 443. This step is required when ufw is active. Allow these ports on the server:

    sudo ufw allow 80/tcp
    sudo ufw allow 443/tcp

    If the cloud provider has a second firewall in its control panel (a "security group"), allow incoming TCP connections on ports 80 and 443 there too. Keep port 8080 closed to the public internet.

    Explain this step: how Caddy gets a certificate

    Caddy asks Let's Encrypt for a certificate for your hostname. This recipe needs ports 80 and 443 open to the internet and your DNS pointing at this server. Opening them in ufw alone is not enough if the provider's firewall still blocks them.

    Tell Caddy where TIER is. Open sudo nano /etc/caddy/Caddyfile and add this site block, using your hostname. Replace the file only if it is dedicated to TIER; preserve existing sites otherwise:

    tier.example.com {
        reverse_proxy 127.0.0.1:8080
    }

    Save and quit with Ctrl+O, Enter, Ctrl+X, then reload Caddy and read its log:

    sudo systemctl reload caddy
    sudo journalctl -u caddy

    You should see certificate obtained successfully for your hostname. Certificate issuance may take time; inspect errors if it does not appear. Press q to get back.

    Check from another computer, outside the server's network.

    curl https://tier.example.com/api/v1/livez
    curl -m 5 http://tier.example.com:8080/api/v1/livez

    You should see the first command print {"status":"alive",…,"version":"v0.6.1…"}. The second must fail because the public cannot reach TIER’s HTTP port. The outside check only shows whether 8080 is reachable from outside. To confirm TIER itself listens only on this computer, run on the server ss -ltn | grep 8080. You should see 127.0.0.1:8080 and nothing on 0.0.0.0:8080 or [::]:8080.

    If it didn't work: no certificate means checking that DNS points at this server and ports 80 and 443 are open in both ufw and the provider's firewall. A 502 from Caddy means TIER is not running on 127.0.0.1:8080. If the second command returns a live response, check the service's address and close public access to 8080.

    Have each teammate change --server in both their ship command and their 15-minute cron line to https://tier.example.com, keeping the same ingest token. Report viewers open the dashboard at https://tier.example.com and paste the viewer token. Shippers who hold only the ingest token cannot read reports. The admin token stays with the operator. Run the updated ship command by hand to check it.

    You should see Per-repo summary:, then Shipped N events to https://tier.example.com.

    For step 4, give GitHub https://tier.example.com/webhook/github, content type application/json, the same secret as --webhook-secret, and the three events listed there. TIER responds to ping with 204; a missing or wrong X-Hub-Signature-256 is 403.

    Cloudflare Tunnel on Ubuntu. cloudflared makes an outbound connection to Cloudflare, which forwards requests for a hostname in your Cloudflare-managed domain. This needs a Cloudflare account but no Caddy. These Linux commands assume systemd. The earlier setup exercised the connector and health checks, not the dashboard clicks; UI labels may change. In the Cloudflare dashboard use Networking → Tunnels, or the tunnel area in Cloudflare One.

    Create a tunnel in the Cloudflare dashboard. Open Networking → Tunnels, create a tunnel of type Cloudflared, and give it a name you will recognise. On Install and run a connector, pick Debian/Ubuntu and your server’s architecture: uname -m gives x86_64 for amd64 or aarch64 for arm64. Use the matching connector option. Run the install commands that screen shows in a terminal on your server. Then copy and run its connector command, including the whole token. It has this form; use the command the dashboard gives you, rather than typing the placeholder:

    sudo cloudflared service install <token>

    Check the connector, on the server:

    systemctl is-active cloudflared
    sudo journalctl -u cloudflared

    You should see Linux service for cloudflared installed successfully after installing the service, then active from the first check and Registered tunnel connection in the log. Press q to get back from the log.

    Add a public hostname. If you set A/AAAA records for this hostname for Caddy, delete them before adding the tunnel's public hostname (the dashboard creates a CNAME for it). In the tunnel's dashboard settings, add the hostname you want to use in a domain you manage through Cloudflare. Set the service type to HTTP and its URL to 127.0.0.1:8080, then save. This page calls your chosen hostname <hostname>. Keep TIER listening on 127.0.0.1:8080 by making the same service-file address change and restart described under Keep TIER on this computer only in the Caddy walkthrough above; keep the admin token, webhook secret and other flags. No ports need opening for the tunnel, and you do not make a DNS record by hand.

    Explain this step: how the tunnel reaches TIER

    The connector makes an outbound connection to Cloudflare, so you do not need to open incoming ports 80 or 443. The dashboard creates the DNS record for your hostname. Cloudflare terminates HTTPS, so it can process the decrypted admin token, usage uploads and webhook metadata. The connector forwards to 127.0.0.1:8080 on this server, where TIER speaks HTTP. Choose this route only if that third-party handling fits your organization’s data rules; raw laptop session files are not uploaded. Keeping TIER on 127.0.0.1 keeps port 8080 from accepting connections from other computers. The admin token and webhook secret still protect TIER's requests.

    Check from another computer, outside the server's network. Replace <hostname> with the hostname you added:

    curl https://<hostname>/api/v1/livez

    You should see {"status":"alive",…,"version":"v0.6.1…"}. Your version will match the TIER build you installed.

    If it didn't work: a connector that is not HEALTHY, or no Registered tunnel connection in its log, means checking that you copied the whole token and that the server can reach Cloudflare outbound. For a 502 or 1033 from Cloudflare, check the connector, that TIER is running on 127.0.0.1:8080, and that the hostname's service URL is 127.0.0.1:8080.

    Check that it survives a restart. With TIER's user service and lingering enabled as in C1, reboot the server:

    sudo reboot

    You should see the same outside health check work again after the server starts. After reconnecting to the server, systemctl is-active cloudflared and systemctl --user is-active tierd should both print active.

    Move the teammates to HTTPS. Follow the teammate migration paragraph in the Caddy walkthrough, using https://<hostname> for your tunnel. For step 4, give GitHub https://<hostname>/webhook/github. With --webhook-secret, a missing or wrong signature returns 403; without the flag, the webhook path is 404.

  4. Step 4. Tell GitHub where to send merge notices. On GitHub, open the repository → Settings → Webhooks → Add webhook, and fill in:

    • Payload URL: https://<your-server>/webhook/github
    • Content type: application/json
    • Secret: the value printed in step 1
    • Which events? choose Let me select individual events and tick all three: Pull requests, Pushes and Workflow runs. Workflow runs matters if the project runs tests with GitHub Actions: it sends their results to TIER, so a qualifying failed workflow reduces credit; TIER does not distinguish tests from deployment or infrastructure workflows. It does not create tests or make them run.

    Click Add webhook.

    Explain this step: what the three events do

    A webhook is GitHub calling your server. Pull requests records an issue-linked merge at quality 1.0. Pushes detects supported reverts from commit messages such as those made by git revert. Workflow runs records qualifying merge-commit CI results. The exact floors, windows and rerun rule are explained under “How merges earn points” above. TIER handles these three event types and GitHub’s ping; ticking more only adds noise. The secret authenticates the payload; a valid signature does not prove that a change is good.

    You should see GitHub send a test "ping" at once and show a green tick under Recent Deliveries with response code 204.

    If a delivery shows 403: the secret in GitHub and the file on the server differ. 404: the server started without --webhook-secret, or the address is wrong. A timeout: GitHub cannot reach the address; check step 3 and the firewall. A successful delivery records the event; team figures wait for its month to seal. Use C1 step 9’s seal --status command and check the contributor floor before treating absent numbers as a delivery failure.

  5. Step 5. Watch one merge land. Wait for the next pull request your team merges in the normal way, one whose branch carries an issue number, merged by someone allowed to merge it. Do not merge anything just to test TIER. You should see a successful webhook delivery in GitHub, with no backfill run. The team's points can appear only when that merge's calendar month closes, the default 14-day grace ends and the month seals, subject to the per-figure k rule. The currently displayed sealed month does not change.

Advantages. While the server is reachable, GitHub sends supported merge, push and workflow events. Qualifying failures matched to recorded outcomes can reduce credit. Nobody re-runs backfill.

Disadvantages. An HTTPS front to set up and maintain, and a shared webhook secret. Deliveries while the server is down are not captured automatically by TIER; inspect GitHub Settings → Webhooks → the webhook → Recent Deliveries and request redelivery after recovery. Stored delivery IDs and merge-commit keys prevent ordinary repeats from adding duplicate outcomes. For CI delivery-order limits, use the technical reference linked under “How merges earn points”. Backfill recovers merges, not CI or revert history, and redelivery cannot revise a sealed month. Laptops must still upload usage.

Documentation

The reference, in the repository.

Repository links below use main, which can move beyond this guide. For release-matched reading, select tag v0.6.1 in GitHub’s file view, or use the running server’s /docs/ for documentation embedded in that binary.

Finance: record what was paid for Spend Leverage

Have the operator post a payment to the running TIER server. The admin token stays with the operator. Finance supplies the paid amounts; report readers use the viewer token, which cannot write. For one developer, developer is the canonical GitHub login, period is the invoice’s calendar month as YYYY-MM, and actual_paid_usd is finance’s paid total in US dollars. Replace every placeholder, leaving no angle brackets; the amount is an unquoted JSON number. The operator runs this localhost command in their own account on the TIER server, using the private token file from C1. If the server is remote, log in there first:

printf 'Authorization: Bearer %s\n' "$(cat ~/.tier/api-token)" |
  curl -i -X POST http://127.0.0.1:8080/api/v1/actual_spend -H @- -H "Content-Type: application/json" -d '{"developer":"<github-login>","period":"<YYYY-MM>","actual_paid_usd":<actual-paid-usd>}'

Success is 201 Created. These entries accumulate; resending the same invoice adds it again. Negative entries are credit memos. For a shared organization invoice, use POST /api/v1/org_actual_spend and replace developer with org, spelled exactly as the roster’s organization field. Each person’s own payment takes precedence for that month; the organization balance after those payments is split evenly across its other active members. Organization membership and monthly allocation determine who receives it.

For Spend Leverage, use month-aligned From and To dates. Payments cover whole calendar months; the default trailing 30 days can compare a partial month of usage with whole-month payments. TIER does not import your invoice or decide tax, currency conversion or allocation policy. Finance supplies USD totals under its accounting policy; read Technical reference, Spend Leverage accounting and the actual-spend API contract before posting shared plans or corrections. Record payments before sealing if they belong in that month’s immutable team report. Labour, business revenue and the value of the software are outside this ratio.

Start here

Understanding TIER

What TIER is and how the number is produced, in three layers: a plain-language short answer, then how the meter works, then the full method.

The full method

The TIER white paper

The whole method in one document: the formula and its units, where the cost and the outcomes come from, attribution, privacy, how to read the number, and the known ways it can mislead.

Read the number

Interpreting the Number

Why a recent period reads low, what low attribution coverage means, the cost-per-point view, and why there is deliberately no good-or-bad band. Read this before you trust a score.

Where it fits

How TIER Relates to DORA

If your team already measures its software work with DORA, SPACE or DX Core 4, three well-known sets of engineering measures, this explains how TIER differs and what it adds: how much merged work you get per dollar of AI usage. TIER has units of points per dollar; combining it into those frameworks’ scores would discard that meaning.

Peer learning

Move Practices, Not Scores

How a team can talk about which working habits help, and compare its results before and after changing one. Every step can use group reports, with the viewer access described in C1.

Law & privacy

Legal & Privacy

For the people responsible for workplace rules and privacy: what employee representatives, data-protection assessments and GDPR Article 22 mean for TIER, and the limits of named measurement. TIER is built for coaching, not for evaluating developers. Per-person figures are not accurate enough to judge an individual. Read the coaching guidance.

The contract

API Compatibility

What the score report (GET /api/v1/scores) and the period comparison (GET /api/v1/scores/compare) contain, field by field; the export columns, which are only ever added to, never removed or reordered; and the rules for what may change between versions, including version boundaries and exceptions to check before upgrading a reporting pipeline. These are routes on your TIER server, not pages on this site.

Security controls

Security

Token checks use constant-time comparison; failed attempts have a configurable rate limit and lockout. GitHub payloads require an HMAC signature. This document gives the code pointers, release verification, TLS/proxy setup, backup protection and limits of the shared token and single-tenant database.

Operations & accounting

Technical reference

Capture paths, supported formats, configuration, attribution, evidence windows, sealing, Spend Leverage allocation and deployment. Use the README’s Operations section for backups and upgrades.

Audit & recovery

Reports and sealing

Manifest export, report verification and the difference between recomputing an unsealed window and verifying a stored sealed month. The API contract covers month selection, comparisons, configuration changes, stalls and permanent gaps.

Capture & data handling

Attribution and capture

Session paths, custom capture configuration, worktrees and attribution rules. Privacy lists what each parser reads and what the database retains; the API contract covers export, correction and erasure.

Prices & finance

List prices and your bill

Price sources, cache rates, unknown-model estimates, overrides and repricing. For payments, use the finance instructions above and the technical reference’s Spend Leverage accounting. Use cases and FAQ explain the decisions the ratio can inform and its limits.

Browse the full docs tree on GitHub →

Read the source

Read the scoring implementation.

One Go program over one SQLite database file. At tag v0.6.1, go.mod lists three direct dependencies: fsnotify, yaml/v3 and modernc.org/sqlite, plus indirect dependencies. The scoring engine, size-label rules and quality resolver implement the calculations; no AI model judges the work.