TIER docs

TIER for engineering managers: common questions

TIER (Token Impact & Efficiency Ratio) links recorded AI spend to merged work and reports outcome points per $1,000. This is an observational association, not evidence that the spending caused the work. This page answers the questions engineering managers ask about it, with the command for each answer. It assumes a TIER server is already running; installing one, and getting spend and merged work into it, are covered in the project README. For what the number itself means, start with understanding-tier.md.

v0.7.0 organizations: Start with what changes for managers. The command recipes use issued keys. Legacy shared/static tokens and tokenless real-server access are retired; issue organization keys when upgrading.

What changes with v0.7.0 organizations?

A node is a group in the organization tree. Its subtree includes that group and every group below it. Use the credential scope map for root, managing node, read-only node, personal, device/upload, federation and the retirement of shared/legacy keys. It links who can create people, issue keys and read data.

Before inviting a team, read these limits:

A node key opens cards for people in its dated branch during the requested window when names_visibility permits. See holder access. TIER is built for coaching, not for evaluating developers. Per-person figures are not accurate enough to judge an individual.

How does developer coaching access work?

Coach the practice, not the person. Grade the work, not the worker.

TIER helps engineers choose the right model, give it the right context, and spend where it pays. A person's page is visible to their personal key; root and holders of node keys whose dated branch contains them can read it when names_visibility permits. Other holders cannot read it. Node-key viewers receive records within their dated branch, so card contents can differ by viewer. The developer and root can read the retained log of who viewed the card, including administrators; another person's node-key viewer receives no view-log entries. Managers may coach from it; no one may ask for figures beyond it. The product cannot enforce that; only your policy or works agreement can. Anyone with host access can read the database; these limits apply at the API.

Issued-key access: Root and dated branch node keys can open coaching cards when names_visibility permits; personal keys retain their own card. Each real read is audited before construction; declarations, static/shared tokens and a positive-spend prerequisite are retired. See identity access for issued-key setup and retention.

TIER is built for coaching, not for evaluating developers. Per-person figures are not accurate enough to judge an individual.

What do I need before I run these commands?

v0.7.0 availability: Coaching cards are available to personal/root/dated-branch node keys with audited reads and raw-data retention; name-free comparisons retain their privacy floor. The separate real-data per-issue report remains future work. tierd demo shows invented views read-only in a private temporary database and cannot load yours.

Three values. Ask the person who runs your TIER server for them.

Save each token in a file that only you can read. These commands work in bash or zsh. The read line asks for the token without showing it on screen or saving it in your shell history: it waits with nothing on screen while you paste, then you press Enter:

mkdir -p ~/.tier && chmod 700 ~/.tier
( umask 077; read -rs t; printf '%s\n' "$t" > ~/.tier/api-token )
chmod 600 ~/.tier/api-token

Do the same for the read-only token, saving it as ~/.tier/read-token.

Open /signin on the server and sign in with your issued root, node or personal key. The browser uses an in-memory server session (60-minute idle and 12-hour absolute expiry); it does not retain the key. Device/upload keys cannot sign in.

The examples pass the token to curl through a pipe (printf … | curl -H @-) rather than on the command line, because on most systems anyone else logged in to the same machine can see a program's command line in the list of running programs (ps). printf is built into bash and zsh, so it never shows up in that list. Real servers require issued keys even on loopback; keep the Authorization header in these commands. The synthetic demo is the tokenless exception and serves invented data.

Every curl here fails on an HTTP error (-f, or --fail-with-body where the error text matters). When the server answers with an error, such as 401 for a wrong token or 400 for a bad request, curl prints The requested URL returned error: 401 and passes nothing on to jq, so an error cannot be read as an empty answer. -f also drops the server's own explanation of the error. To see it, keep the printf line, run the curl part with -sS --fail-with-body in place of -fsS (curl 7.76 or later), and leave off the | jq … stage: jq would swallow the explanation, and a wrong token can then print something that looks like a real, empty answer.

The examples also use jq, a free tool that picks fields out of JSON (brew install jq on a Mac, sudo apt install jq on Debian or Ubuntu). The full setup is in the project README under What you need before you start.

How do I pilot TIER on my team?

Start with one question: which tools do the engineers use? TIER can read local usage records from the Claude Code, Codex CLI, Opencode and Meta Muse Code CLIs on each laptop. Claude Code, Codex and Muse write session logs; Opencode keeps its per-call records in a local SQLite store. These readers do not capture Claude usage inside Cursor, Copilot or the claude.ai website. Introduced in v0.6.2, two supported, separately configured server pollers cover GitHub Copilot organisation AI-credit gross spend (collectors.copilot_billing) and usage on a Cursor team plan with Admin API access (Teams or Enterprise; Cursor decides which plans include it) (collectors.cursor_admin). Copilot needs an installed GitHub App with Administration: Read-only for spend (preferred), or an org-admin token; organization GitHub Copilot Business: read is needed only for counting through root mappings; Cursor needs a team Admin API key. Individual Copilot Pro and individual Cursor accounts are not covered by these pollers; ChatGPT Team remains uncaptured. Root mappings bind Copilot numeric IDs and Cursor emails to people for counting; aliases continue to assign spend. Vendor IDs are never stored on usage events. Existing App installations must accept the updated permissions. Neither poller links spend to a PR or issue. Cursor skips BYO-key requests and their Cursor fee; provider spend is captured by anthropic_admin or openai_usage only when the matching poller is configured for the billed organisation. See the source requirements and pricing limits. Missing usage can make the ratio read high when some cost is recorded. Your Anthropic account's billed totals per engineer cannot, on their own, be split by PR, issue or feature: they contain no work linkage.

Plan the first report around a full calendar month. Recovery works only while the source files still exist. Claude Code deletes a session file after cleanupPeriodDays, a Claude Code setting that is 30 days unless you change it, so check yours (Claude Code data retention). A first pilot with default Claude Code retention can usually recover only about a month of cost; the other tools depend on their own retained files. ship and backfill default to 90 days, which does not recover deleted logs. Set the same recent --since when loading costs and merged work, and check the report's cost-capture horizon; reading a name-free comparison requires a closed calendar month (step 6).

v0.7.0 uploads: Give each laptop its developer's bound upload credential for POST /api/v1/events. Shared ingest credentials are retired. Historical shared uploads remain readable within authorized holder scope but supply no counted identity. See the trust limits and the shared-token retirement runbook. The separate real-data per-issue report remains future work; tierd demo previews invented data in a private temporary database that cannot load yours.

Each developer uses a bound personal/upload key. Root and node keys within the dated branch can read the same audited coaching card as its holder when names_visibility permits. Declarations and positive-spend requirements are retired. See identity access for host bootstrap and key issuance.

Install tierd via quickstart. Start the server to create its database, issue root on the host, then create holder keys through Organization. Save shown-once secrets privately.

Start this walkthrough in the current UTC month with a fresh database. The backfill and ship commands below start at that month so startup cannot freeze older outcomes before their matching spend and roster arrive. Writable serve closes due months at startup and every minute, 24 hours after month end. Finish roster import, shipping and aliases before then; later uploads cannot revise frozen costs or census. This sequence cannot bootstrap complete historical reports: use offline score --outcomes for past work. For branch reports, use the first full month after the roster takes effect.

  1. Start one reachable server. Inputs: issued holder keys (the README upload-token recipe explains the send-only one); set TIER_HOST to the HTTPS address you will put in front of this server.

    tierd serve --addr 127.0.0.1:8080 --db "$HOME/.tier/tier.db" \
      --trusted-proxy-cidr 127.0.0.1/32
    

    serve speaks plain HTTP, so run an HTTPS reverse proxy on the same machine and make it the only way in; the loopback bind keeps the plain listener off the network. --trusted-proxy-cidr lets the failed-login lockout see each client's own address instead of the proxy's, so one laptop with a wrong token cannot lock out the team.

  2. Load the team roster. Export a CSV with header developer,team,division,org, one row per engineer. Use the same GitHub login for developer as in backfill and shipping; division and org may be empty. Import through the running server with a root key:

    tierd hierarchy import --server "$TIER_HOST" --api-token @"$HOME/.tier/api-token" \
      roster.csv
    

    Name-free comparisons use the frozen census (default k 5, minimum 3): a rostered, non-bot person needs positive qualifying spend and a positive outcome count on the dated path to count; a direct push can supply the outcome. Bound uploads, --watch-as and root-mapped vendor evidence qualify; shared/legacy/manual spend does not. One tree decision covers additive figures; small groups can fold into Other and complementary suppression can withhold additional rows. There is no separate contributor floor for each spend/points/paid measure. Load the roster before closing; importing it later does not revise a closed month.

  3. Load merged PRs. Inputs: create a GitHub token in GitHub Settings → Developer settings → Personal access tokens → Fine-grained tokens, scoped to the repository with Pull requests: Read-only, and save it without echoing:

    (umask 077; read -rs t; printf '%s\n' "$t" > ~/.tier/github-token); chmod 600 ~/.tier/github-token
    tierd backfill --repo acme/app --since "$(date -u +%Y-%m-01)" \
      --token @"$HOME/.tier/github-token" --db "$HOME/.tier/tier.db"
    

    Run this on the server machine, stopping serve first and restarting it after all repositories are loaded: backfill writes the same database directly. Keep --since at the current UTC month's start when repeating this step. An elapsed month past its grace can close on restart before step 4 supplies its matching costs and counted contributors; later shipping cannot repair it. For live merges instead, follow webhook setup: configure the shared secret with serve --webhook-secret @file and send GitHub deliveries to POST /webhook/github. Branch names or PR bodies must name the issue (conventions); issue ids come from branch and PR text and can be misfiled (technical reference §5.1).

  4. Provision each person's bound upload key, then ship their laptop's usage. After loading the team map, run this on the server host for each engineer, using their GitHub login so spend joins their merged PRs:

    tierd tokens issue --db "$HOME/.tier/tier.db" --subject jsmith --label laptop --upload
    

    Deliver the show-once secret through your secure secret channel to that person's laptop as ~/.tier/upload-key, mode 0600; repeat for every person before shipping. Each key is bound to its subject; historical shared uploads supply no counted identity for k. See the identity runbook. TIER_HOST is the server's HTTPS base address and ~/src/app is the checkout.

    tierd ship --server "$TIER_HOST" --repo "$HOME/src/app" --since "$(date -u +%Y-%m-01)" \
      --developer jsmith --api-token @"$HOME/.tier/upload-key"
    

    Claude Code is read by default. Add --codex-rollout, --opencode or --muse only for tools that engineer uses; those collectors are off by default. Repeat --repo for more checkouts. On forks, set the upstream identity with --repo-slug "$HOME/src/app=acme/app". Opencode currently captures only the Z.ai coding plan; it records no branch, so its spend remains unattributed to issues. Run every 15 minutes via the documented cron/launchd setup, preserving these flags; move a fixed --since forward over time. Re-sends deduplicate. Check exit codes and the per-repository shipping summaries.

  5. Optionally record billed totals. Inputs: the account's actual billed USD per engineer and calendar month, using the same developer id as above. This is a manual admin POST (API reference):

    printf 'Authorization: Bearer %s\n' "$(cat ~/.tier/api-token)" |
      curl -fsS -X POST -H @- -H 'Content-Type: application/json' \
        -d "{\"developer\":\"jsmith\",\"period\":\"$(date -u +%Y-%m)\",\"actual_paid_usd\":100}" \
        "$TIER_HOST/api/v1/actual_spend"
    

    Posts add together; do not re-post a bill. Spend Leverage is captured list-price cost divided by actual paid spend; use whole calendar months for this cross-check. It does not allocate the bill to issues or prove completeness.

  6. Read the closed month. A writable server closes each UTC month automatically once its 24-hour grace has elapsed, at startup or on a subsequent minute pass. Finish loading history, roster and aliases promptly: later raw input does not revise frozen monthly figures. Named branch reports expand to their people; comparisons retain the k floor. Check actual close time with tierd doctor or root's /api/v1/org/report/status.

Then read the monthly comparison, using an authorized key (replace 2026-10 with an available closed month):

printf 'Authorization: Bearer %s\n' "$(cat ~/.tier/read-token)" |
  curl -fsS -H @- "$TIER_HOST/api/v1/scores?period=2026-10"

The report gives group cost at list prices, weighted points and TIER (points per $1,000), subject to the k floor above. Open TIER_HOST for the dashboard, sign in with your issued key and select the same closed month. Capture Fidelity is not completeness: it measures how captured spend arrived, and says nothing about usage TIER never saw.

Can I see my developers' names?

Choose the organization's names_visibility setting for named reporting and coaching access: on permits root and dated branch node keys; top_admin_only permits root; off disables that access for both. Personal keys retain their own figures and coaching card under every setting. Choosing off still leaves developer identifiers available to root and managing node keys through the people inventory, within their administrative scope. Root data-subject exports remain audited and available. See identity access. Server reporting modes are retired. serve takes no aggregation setting; --aggregation, nonempty TIER_AGGREGATION, and the YAML aggregation key are rejected. Root keys read everyone, node keys read their dated branch, and personal keys read their holder. Organization, Team, Person and Models views remain available, including group-to-person expansion. Name-free monthly comparisons retain the k floor (default 5, minimum 3), complementary suppression. Archived raw-manifest verification retains its original pinned arithmetic; that pin cannot select a server access policy.

Root and dated branch node keys can open coaching cards when the names setting permits; personal keys can open their own. The synthetic demo shows every invented developer's row. Raw bulk /api/v1/events is denied on real servers; compliance exports are audited. TIER is built for coaching, not for evaluating developers. Per-person figures are not accurate enough to judge an individual. TIER cannot enforce that stance; your policy or works agreement (a written agreement with your works council or other employee representatives) has to support it (see How do I use this to help my team get better?).

Card access. Root and dated branch node keys can open coaching cards when names_visibility permits; personal keys retain their own card. Each real read is audited before construction; declarations, static/shared tokens and a positive-spend prerequisite are retired. See identity access for issued-key setup and retention.

The #1371 raw-data retention engine is built, with a 90-day default and up to 365 days for an annual coaching cycle. In real writable servers, aged spend and merged-work rows are pruned into a name-free roll-up; the permanent cutoff applies to subsequent named reads. The coaching card shows practice levers first and TIER last. The view log's retention is capped at the developer retention window whenever developer retention/pruning is enabled. Raw GitHub webhook bodies (if the webhook is on) can include a contributor's name or email address. They are deleted automatically once they are about 90 days old, or sooner if more than 50,000 are stored, and an erase does not rewrite them (privacy.md).

Name-free monthly comparisons have a minimum group size, called k: the fewest people a group must have before TIER shows it by name. k is set with --k-anonymity; the default is 5 and the server refuses anything below 3. The monthly census counts each person once across model rows and paths. A rostered, non-bot person needs both positive qualifying spend and a positive outcome count on the dated path; a direct push can supply the outcome. Qualifying spend is bound upload, --watch-as server-watch or root-mapped vendor-attributed evidence. Mutable aliases do not lend another subject's evidence. Shared/legacy/manual spend and outcome-only activity can contribute amounts without adding a counted person. The census is frozen at close and survives erasure as anonymous counts.

Publication uses the larger of the close's k and the reading server's k, plus complementary suppression; a shown group also needs at least two members. One full-tree decision governs the additive figures, without separate spend/points/paid contributor floors. Paid and realtime amounts do not decide which groups publish. Small groups can fold into Other; siblings or parents may also be withheld to prevent subtraction. Inspect each row's Disposition in comparison.Nodes and comparison.Other; only shown rows leave the server. comparison_withheld signals withheld or folded figures, even when other groups are shown. Paid outside activity nodes sits on the name-free company line. Spend Leverage requires positive paid spend; TIER and cost per point require applicable cost and points.

GET /api/v1/scores?period=YYYY-MM returns a monthly comparison manifest, with comparison, group_labels, group_financials, disclosures and frozen close metadata. It rejects arbitrary date, team and work-type filters. This replaces the old teams, total and data_quality shape on real servers. Monthly work-type and corrective-work panels have their own frozen publication guards; see the API contract.

Named score routes return the coaching card to authorized root, dated-branch node and personal keys, subject to names_visibility. They do not require positive verified spend or viewer declarations. Reads are audited before construction. Raw identifiers are retained under the built retention policy; bound uploads use the key's subject, --watch-as declares the local Claude watcher's subject, and other local capture can use the OS login or an override. Merged PRs use the author's GitHub login. Map different identifiers belonging to one person before future closes; already-closed activity and census stay frozen. Billing revisions and identity-removing erasure/retention pooling are the supported exceptions to monthly immutability, with totals conserved by pooling.

Use Organization → Team for a named branch report and live date windows, or monthly comparisons for name-free trends. Server reporting modes and legacy shared/static tokens are retired.

Can I keep names and teams in a file and load them?

You can keep teams in a spreadsheet and load them as a CSV file. There is no field for a real name: TIER stores only the identifiers it is given, so keep the mapping from login to real name in your HR system or your own spreadsheet. (If someone's username is their real name, such as jane.smith, that is what TIER stores.)

What exists today (these compatibility administration routes need an issued root key):

Request What it does
POST /api/v1/developer_alias Tells TIER that two identifiers are the same person.
GET /api/v1/developer_alias Lists every alias.
DELETE /api/v1/developer_alias/{alias} Removes one alias.
POST /api/v1/org_hierarchy Loads a list of developer → team, division, org in one call. tierd hierarchy import reads a CSV file and sends its rows here as JSON.
PUT /api/v1/org_hierarchy/{developer} Adds or changes one developer.
GET /api/v1/org_hierarchy Returns everything loaded so far.

Step 1: join each person's two identities. alias is the identifier on the spend side: the identifier actually recorded on their spend, which may be the issued key subject, --watch-as name, OS login or a configured override. canonical is their GitHub login, shown on their GitHub profile page. Do this once for each person whose two identifiers differ:

printf 'Authorization: Bearer %s\n' "$(cat ~/.tier/api-token)" |
  curl -fsS -X POST -H @- -H "Content-Type: application/json" \
    -d '{"alias":"jane-laptop","canonical":"jsmith"}' \
    "$TIER_HOST/api/v1/developer_alias"
# prints nothing when it works (HTTP 201)

One GitHub login can have several aliases, for example one per laptop; send one request for each. An alias applies to spend recorded before you added it when TIER computes a live score or closes a month, because it joins identities when computing scores. Already-closed months retain frozen results and are not recomputed. An alias does not change which team that earlier spend counts toward (see below).

If you skip this, cost and work can remain on separate identities. Monthly comparison diagnostics report whole-company cost-only and outcome-only counts under quality.whole when that population is published; they do not list unjoined names. Named coaching cards use issued-key scope and audited reads, with no positive-spend prerequisite. Missing merged work may simply mean the person has not merged anything in the window yet, so check source records before adding an alias.

Step 2: write the team file. Make a spreadsheet with this header row and one row per developer, then export it as CSV (Google Sheets: File > Download > CSV; Excel: Save As > CSV UTF-8):

developer,team,division,org
jsmith,platform,infrastructure,acme
bdiaz,payments,product,acme

Step 3: load it. tierd hierarchy import reads the CSV, checks it, and sends it to the server. It needs the tierd program on your machine (see the README) and an issued root key:

TIER_API_TOKEN=@$HOME/.tier/api-token \
  tierd hierarchy import --server "$TIER_HOST" teams.csv
# hierarchy import: 2 rows written

Add --dry-run to check the file without sending it. The load is all or nothing. If one row is bad, nothing is written and the error names the row with its CSV line, for example line 4: developer is empty. The same person on two rows, under any id or alias, is rejected the same way. One load takes up to 1,000 rows and 1 MiB; split a bigger file into several, and never put one person in two files. Loading the same file again is safe; it overwrites. A load only adds and updates: a developer left out of the file keeps their current team, and no request just takes a developer off a team; you can only move them to another. If the command says the outcome is unknown, or a request fails with HTTP 503 (another write held the database for a moment), run the same command again.

Without tierd, send the same rows to POST /api/v1/org_hierarchy as a JSON array of objects with the same four fields. Save them in a file called teams.json; leave out division or org if you do not use them:

[
  {"developer": "jsmith", "team": "platform", "division": "infrastructure", "org": "acme"},
  {"developer": "bdiaz", "team": "payments", "division": "product", "org": "acme"}
]

Then send it from the same directory:

printf 'Authorization: Bearer %s\n' "$(cat ~/.tier/api-token)" |
  curl -sS --fail-with-body -X POST -H @- -H "Content-Type: application/json" \
    --data-binary @teams.json "$TIER_HOST/api/v1/org_hierarchy"
# success response (HTTP 201) includes "accepted":2 plus membership-decision lists and counts

This uses --fail-with-body rather than -f so that an error prints the server's explanation. The explanation names the row by its position counting from 0 (org_hierarchy[3] is the fourth object). The request and response shapes are in api-compatibility.md.

The server resolves each developer through the aliases it already knows at the moment you load, so run step 1 before step 3. To move one person later (this resolves aliases the same way):

printf 'Authorization: Bearer %s\n' "$(cat ~/.tier/api-token)" |
  curl -fsS -X PUT -H @- -H "Content-Type: application/json" \
    -d '{"team":"payments","division":"product","org":"acme"}' \
    "$TIER_HOST/api/v1/org_hierarchy/jsmith"
# prints the stored row

TIER keeps a dated history of team membership. Every load and every move is dated by the server's clock at the moment it is made; no request field, file column or flag can set that date. Team and division rows are computed from the membership each developer had at the time of each event: spend at the event's own timestamp, merged work at the time it merged, an invoice at the start of its month. So a move affects only the future. A person's earlier spend and merged work stay with the team they were in when it happened, in every window and in both periods of a comparison. Aliases follow the same rule. Each identifier keeps its own dated team history: adding an alias puts that identifier in its person's team from that moment, pointing it at someone else moves it to their team from that moment, and deleting it places it by its own assignment from that moment: the team you assigned that identifier itself, if it has an assignment of its own (for example one you loaded before you made it an alias), and no team if it has none. Spend and merged work recorded under the identifier before the change stay in the team it was in then, or Unplaced if it had none. When you move a person, all of their aliases move with them, also from that moment. A server clock that has been set back is the one exception: a move made then is dated at the earlier time, so spend and merged work already recorded after that time move to the new team. TIER refuses a move (409) only when the clock reads earlier than the person's current assignment began, so keep the host's clock synchronised. If an issue's spend came before a move and it merged after, the old team shows the spend and the new team shows the merged work. History from before a developer's first assignment stays in Unplaced even after you assign them. When you upgrade to the version that added this, the assignments you had already loaded are kept for all past data; only people you assign after the upgrade start on the day you assign them. Request and response shapes for all of these requests are in api-compatibility.md.

Where are my teams' scores?

Use Organization → Team to read a named branch and expand it to people under your issued key's scope and the names setting. For name-free trends, read a closed monthly comparison. /api/v1/scores defaults to the latest close; ?period=YYYY-MM selects another. A missing close returns 404.

For September 2026:

printf 'Authorization: Bearer %s\n' "$(cat ~/.tier/read-token)" |
  curl -fsS -G -H @- "$TIER_HOST/api/v1/scores" --data-urlencode "period=2026-09" |
  jq '{month: .scope.month, closed_at: .close.closed_at,
    horizon: .disclosures.horizon, partial_input: .disclosures.partial_input,
    withheld: .comparison_withheld, labels: .group_labels,
    nodes: .comparison.Nodes, other: .comparison.Other}'

disclosures.horizon.window_predates_cost_capture: true means the month starts before captured spend. Choose a later closed month beginning at or after disclosures.horizon.cost_coverage_safe_since. Unknown/unrecorded horizon status does not establish complete capture. disclosures.partial_input and origin_evidence disclose retention and capture loss as frozen at close.

Match a group's ID through group_labels. Only rows with Disposition: "shown" publish Figures.Points and Figures.CostMicro (integer micro-dollars), plus ranked. TIER is points divided by cost in thousands of dollars; the API's group_financials supplies paid spend and related ratios. Other contains synthetic folds. Hidden rows are omitted; comparison_withheld signals withholding or folding and can accompany shown rows. An absent named row can reflect no activity, folding or withholding; absence alone does not establish an empty month.

In the synthetic demo, the dashboard lists invented developers and has a table headed "Where the points and spend went, by team". It splits the company total by team, in team-name order, with a "No team" row for invented developers without a team. The demo API exposes the same split in team_rollups and rejects ?period= with HTTP 400. These team_rollups remain a synthetic-demo example; real-data named branch reports use issued-key scope, and monthly comparisons use the frozen census and complementary suppression described in identity access. The following command is for that synthetic demo only; point TIER_HOST at it, replacing since with the dashboard's From date; if To has a date, add --data-urlencode "until=…" in YYYY-MM-DD form:

curl -fsS -G "$TIER_HOST/api/v1/scores" --data-urlencode "since=2026-08-28" |
  jq -c '{window_predates_cost_capture: .data_quality.window_predates_cost_capture,
    cost_coverage_safe_since: .data_quality.cost_coverage_safe_since},
    ((.team_rollups // [])[] | {team, unassigned, tier, weighted_points, total_cost_usd, ranked})'

The dashboard defaults to 30 days back; the API defaults to midnight UTC 90 days ago through now. Match the window to compare figures. Spend counts when it was spent, merged work when it merged. In the synthetic demo only, add --data-urlencode "team=Platform" to the curl command and read .team in jq for that team's roll-up; team_rollups stays unchanged. If data_quality.window_predates_cost_capture is true, move since to data_quality.cost_coverage_safe_since or later. Both fields are absent (printed as null above) when TIER had no coverage start to report — no spend recorded yet, or the server could not read it. The no-team row has "unassigned": true and no team name. It also holds spend that could not be tied to any person, such as proxy calls that named no developer, recorded under unattributed.

Monthly comparisons refuse ?team=, since and until with a 400; they accept closed months. Named branch reports have separate scoped routes and live date windows. In the synthetic demo every invented developer is named, and ?team= selects an invented team.

Read ranked before you quote a team's number. Despite its name, it does not put anyone in order; it says whether there is enough evidence to read the number. ranked: false means the team has too little evidence behind it in this window: fewer than 3 merged outcomes (normally a merged pull request that names an issue), less than $5 of spend at list price, or any one merged outcome whose developer recorded fewer than 1,000 AI tokens (input, output and cache tokens together) against that repository and issue in the 14 days ending at the latest merge reusing that issue among the outcomes being scored. Reused issues share this window; an older merge can therefore be flagged too. The number is still reported, but do not present it as a headline figure.

What happens when TIER cannot tell which merge a revert undid?

When a footerless revert's issue matches several eligible outcomes in the same repository, TIER penalises no one for that check and marks each candidate outcome unknown (revert ambiguous). Its numeric quality stays as it was; a clean outcome stays at 1.0. The JSON outcomes export and quality-event exports expose the unresolved status; the dashboard does not display it. An unresolved commit footer never falls back to guessing by issue. If there is no qualified candidate, TIER logs a warning and increments a diagnostic counter but has no outcome to mark unknown. Unresolved checks are never quality evidence and do not count toward evidence share. These checks do not establish complete quality coverage. The technical reference gives the repository, timestamp and merge-push limits. Corrective-work accounting (Event 3) is built and off by default; it affects live scores when the operator enables it on their installation. See enabled operation.

How do the team scores add up to one company score?

TIER pools everyone's weighted points and list-price cost, then divides once to get points per $1,000 of AI spend for the team or company. Each merged GitHub PR earns points from the size label on its pull request (from 0.5 for size/xs to 8 for size/xl), or from an estimate when there is no label, times a quality factor that can drop, for example when the change is reverted for a code problem (rubric.md). Spend is priced at published list prices (pricing-philosophy.md), not at what you were billed: discounts and subscription fees are not applied. What you actually paid can be recorded separately: monthly manifests put paid spend and Spend Leverage in group_financials (named reports use results.financials). The synthetic demo uses actual_paid_usd and spend_leverage. Neither changes TIER. For a team, and for the company total, TIER adds up everyone's points, adds up everyone's cost, and divides once. Every developer counts, whether their own row is ranked or not.

Here is why that matters, with made-up numbers:

(illustrative) Weighted points List-price cost TIER (points ÷ cost in thousands of dollars)
Team A 60 $2,000 60 ÷ 2.00 = 30
Team B 10 $250 10 ÷ 0.25 = 40
Average of the two scores (30 + 40) ÷ 2 = 35
Pooled, which TIER reports 70 $2,250 70 ÷ 2.25 = 31.1

The company spent $2,250 and got 70 points, which is 31.1 points per $1,000. Applying the average of 35 to the $2,250 spent would imply 35 × 2.25 = 78.75 points; the teams delivered 70. The average gives Team B, which spent one eighth as much, the same say as Team A. Pooling counts each team in proportion to what it spent. A team's own score is built the same way from its developers: summed points over summed cost, not an average of developer scores.

The monthly comparison's whole row pools the company's recorded activity, including Unplaced spend, and publishes only when its tree decision is shown. Complementary suppression can withhold the whole row as well as groups; it does not necessarily withhold every row at once. Inspect comparison.Nodes, comparison.Other and comparison_withheld. The synthetic demo uses total.

The pooled total mixes every kind of work, so it is not a fair way to compare teams that do different work. For real data in v0.7.0, use a permitted subject's coaching card to follow their own trend within a work type. The descriptive work_types score array remains a synthetic-demo surface; monthly manifests use a separate guarded work_types panel. It scores each category (feature, bug, security, incident, tech-debt, research, compliance) separately; compare bug work with bug work. How a pull request gets its category is in rubric.md. There is also no absolute "good" TIER. The fair comparison is a team against itself over time, under the same rules. Monthly manifests stamp the frozen rules in close.rule and close.policy, and price identities in disclosures.prices with used_price_versions; federated evidence carries each origin's stamps in origin_evidence. The synthetic demo uses rubric.version and price_table.version. Changed rules or prices can move a new observation without changing the work; upgrading does not rewrite closed months (rubric.md).

Why does most of our spend say "no issue attached"? Did we never merge that work?

TIER retains spend it could not link to an issue number as "unattributed." The work may still have merged; linking its spend and recording its merge are separate steps. See below for how each gets an issue number.

Spend and merged work meet on the issue id (together with the repository, so issue 42 in two repositories stays two issues):

Which branch gets recorded. With worktree attribution off (the default), TIER uses the branch Claude Code records: the branch of the directory the session was started in, not of the directory where the work happens. Switching branches in that same checkout (git checkout -b feature/42-login) is picked up from the next message. Moving the work elsewhere is not: a session started in the main checkout that edits a sibling worktree (git worktree add ../app-42 -b feature/42-login) keeps recording main, and so does every subagent it starts, because a subagent takes its parent's branch. That spend lands in unattributed:main even after the pull request merges and scores for issue 42. The fix is to start the session inside the worktree on the issue branch (cd ../app-42 && claude); the subagents it starts then record that branch too. --worktree-attribution is an opt-in preview that infers the worktree from the file paths a session's tool calls name, so it reads more of the session file than the default (privacy.md); read how-it-works.md before turning it on. Parents no longer carry (#1016); only message groups labelled isSidechain:true can use carry. Worktree attribution stays opt-in and off by default. Activation requires recorded gates, the operator's security rulings and an explicit operator decision. No new accuracy figures have been measured.

Spend that finds no issue is kept, labelled with the reason:

Label What happened
unattributed:main The recorded branch was main or master. TIER does not guess an issue here, and skips the commit check too, even if a pull request merged minutes later: a guess could credit the spend to the wrong issue, and TIER treats that as worse than leaving it unlinked.
unattributed:branch-without-issue A named branch with no issue number in it (scratch, wip-refactor) and no matching commit.
unattributed:detached-head Git was not on any branch (a "detached HEAD", which is what you get after checking out a single commit or tag), or no branch was recorded at all. Opencode sessions always land here, because Opencode does not record a branch.
unattributed:foreign-repo Only with --worktree-attribution on: the work was in a worktree of a repository TIER was not configured to watch. The other repository is never named.
unattributed Plain unattributed, with no reason after it: spend from a source that never sees a branch. That is TIER's proxy (a relay an app can send its AI calls through so TIER meters them) when the app sends no X-Tier-Issue header naming the issue, or an org-level billing feed (a poller that reads Anthropic's or OpenAI's organisation-wide usage report). Setup for both is in the README under Capturing tokens.

So a pull request can merge, count as finished work, and still leave the spend that produced it in one of these buckets. The reverse also happens, and it does lose merged work: a pull request with no issue reference in its branch or description records no outcome at all (conventions.md).

Unattributed spend stays in the cost and earns no points. The pooled scores (a developer's, a team's, and the company total) divide all points by all spend, so for them it makes no difference which issue the spend is tied to. What can move them is the merged work behind it. If the spend paid for a pull request that merged with no issue number, TIER has no record of that work, so the score reads lower than it should. If the pull request did name an issue and only the spend was recorded on main, the merged work still counts in full and the pooled score is not skewed; the spend just is not tied to its issue, so that issue's own cost reads low. The dashboard's low-coverage note says the same: if some unlinked spend paid for merged work with no issue number, read the score as a floor. The per-type scores in work_types are different: they count only spend tied to an issue of that type, so they read high while much spend is unlinked.

Where to see it:

What raises attribution:

  1. Put the issue number in every branch name: feature/42-login, fix/42, fix/TIER-99-crash (the tracker form works only with TIER configured as a tracker key, for example --tracker-keys TIER). The accepted patterns are in conventions.md. Avoid a bare four-digit number from 1900 to 2099; TIER reads it as a year.
  2. Start the AI session in the checkout that is on the issue branch: switch branches there first, or cd into that branch's worktree before you start. A session started in the main checkout keeps recording main for work it does in another worktree, as described above.
  3. Put closes #42 in the pull request description, so the merged work is linked too.
  4. Decide how much work on main you want. Some of it can be planning and review. TIER keeps it in the cost and shows it as its own share, so the team can decide whether that amount is intended.

These practice changes affect future capture. There is no supported correction of an already assigned issue; renaming a branch does not move a wrong assignment. Server-side PR and branch fills can assign previously unlinked rows without overwriting an assigned issue or revising closed months.

How do I use this to help my team get better?

See availability. v0.7.0 provides name-free cache-share levers over closed months and audited coaching cards with raw-data retention.

TIER is built for coaching, not for evaluating developers. Per-person figures are not accurate enough to judge an individual. The positioning is grade the work, not the worker, by policy rather than by mechanism. The built coaching card uses issued personal/root/dated-branch node keys and audited reads; retention runs at startup and daily; the coaching stance will still depend on your policy.

Do not rank developers against each other or build a leaderboard from these figures. In the synthetic demo the dashboard lists invented developers in identifier order and never sorts them by TIER; a row with too little evidence stays in its place with a "below evidence floor" tag. For real data today, compare a team with its own closed months, accounting for work mix; use a permitted subject's card for their own trend within a work type. Before the number itself, read the available coverage warnings and the bounds' limitations. In the synthetic demo, #1233 withholds intervals when the row's own attributed-cost share is below 50%. Each ranked invented developer row at or above that share carries bootstrap bounds (ci_low and ci_high, drawn as a whisker on the dashboard bar; nominal 95% percentile bootstrap; coverage unvalidated), and team rows and the company total carry none.

Nominal 95% bounds. Coverage is unvalidated: in constructed tests where several outcomes share one issue, they covered the true value less often than 95%. When shown, the bounds are the 2.5th and 97.5th percentiles of the stated bootstrap resampling procedure. Their 95% repeated-sampling coverage has not been validated. They exclude uncertainty from uncaptured usage, incorrect attribution and unattributed spend. Spend is counted when usage happened and points when the work merged, so work that crosses a period boundary can move the ratio either way. Team scores carry no interval, and month-to-month noise is large: TIER has not established how large a change must be to stand out from ordinary variation. Treat a small difference as no evidence either way, and a large one as a prompt to investigate, not a measured effect. Group reports do not disclose exact cost-attribution or quality-evidence shares. Capture-fidelity and cost-horizon warnings are different signals; they do not establish complete capture or observed quality coverage. 'Not shown' is neither a clean bill of health nor evidence that coverage is low. Ask the operator which diagnostics were run, what they found, and which capture or attribution gaps remain. When shown in the v0.6.1 synthetic demo (#1233), the interval is labelled "excludes uncertainty from unattributed spend". The floor uses the row's cost paired with resampled outcomes; exactly 50% passes. Team and division rows carry no interval and reveal no floor status.

peer-learning.md is the full method for a team. In four steps:

  1. Hold a team review using /api/v1/scores?period=YYYY-MM. Monthly comparisons publish group_levers[group_id].cache_read_share, the share of frozen input-side tokens read from the provider's cache. Each shown group needs k counted people with spend and merged work; any failing group withholds all levers. Each cache partition also needs k eligible positive-input contributors with cache reads and k without. Bots, spend-only people, zero-input rows, erased evidence and duplicate model rows cannot add seats. k is at least 3; withheld shares are absent. See peer-learning.md. Ask what explains the cache share. v0.7.0 ships the verified, subject-bound coaching card with raw-data retention: cache reuse, premium-model spend (models priced at $4 or more per million input tokens), no-issue spend including main, and within-work-type yield are per-person levers. Team rows expose cache share as their practice lever; monthly work-type panels have separate publication guards, while detailed model and token-class tables remain outside the name-free comparison. If attribution is low, fix it first: work-type scores omit unlinked spend and can read high.

  2. Treat a move to a cheaper model as a hypothesis to investigate. Compare similar work over finished periods, record other changes (task mix, acceptance criteria, pricing and capture), and inspect acceptance and repair evidence beside cost. A before/after change in TIER cannot show that the model change caused the difference. TIER associates merged outcome points with models by cost, with an equal-share allowance for zero-cost attributable models; this is association, not causal credit, so it cannot say whether the cheaper model did the work as well.

  3. Compare the team's score before and after the change, over two periods that have both ended and are the same length. Spend and merged work are each counted by their own date, so a period's edges can move its score either way. Work paid for before the start that merges inside the period adds points without its spend, and reads high. Spend near the end whose work merges after the period adds cost without points, and reads low. A short period has less in the middle to outweigh its edges, so it moves more (interpreting-the-number.md). Real-data named branch reports support live windows under issued-key scope; monthly comparisons use closed months. The following command is for the synthetic demo only, with TIER_HOST pointing at that demo. The demo's merges sit 7 to 19 days before the day it started, so with that day as S, replace BEFORE_START with S minus 22 days, CHANGE_DATE with S minus 14 days and AFTER_END with S minus 6 days, all as UTC dates:

    for period in BEFORE_START,CHANGE_DATE CHANGE_DATE,AFTER_END; do
      curl -fsS -G "$TIER_HOST/api/v1/scores" --data-urlencode "team=Platform" \
          --data-urlencode "since=${period%,*}" --data-urlencode "until=${period#*,}" |
        jq -c '{tier: .team.tier, ranked: .team.ranked, predates: .data_quality.window_predates_cost_capture, safe_since: .data_quality.cost_coverage_safe_since}'
    done
    

    Each period is a start date and an end date, written YYYY-MM-DD, in UTC. The start date is included and the end date is not, so CHANGE_DATE,AFTER_END covers 8 days, the same length as the first period.

    For real-data name-free comparisons, read two closed months instead:

    printf 'Authorization: Bearer %s\n' "$(cat ~/.tier/read-token)" |
      curl -fsS -G -H @- "$TIER_HOST/api/v1/scores/compare" \
        --data-urlencode 'period_a=2026-09' --data-urlencode 'period_b=2026-10' |
      jq '.compare[] | {month: .scope.month, horizon: .disclosures.horizon,
        withheld: .comparison_withheld, labels: .group_labels,
        nodes: .comparison.Nodes, other: .comparison.Other}'
    

    The response's compare array contains two monthly manifests, each with its own frozen close, disclosures and tree decision. Match group IDs through group_labels and read only shown rows. ranked must be true in both; otherwise there is too little evidence to read the change. A missing close returns 404. Unknown horizon status does not establish capture; if disclosures.horizon.window_predates_cost_capture is true, choose a month beginning at or after cost_coverage_safe_since in the same horizon block.

    Monthly closes freeze observations; they do not align merges with all of their spending. Months differ in length and can have different work crossing their boundaries. Record differences in repository scope, membership, work mix, capture and rules before interpreting a change. If those differences are unknown or substantial, report the observations without calling the difference an improvement or deterioration. The comparison manifests carry no significance test or team bootstrap bounds; a change is a prompt to investigate, not a measured causal effect.

  4. Write the practice down in your team's own runbook: what changed, which figure you watched, what happened, keep or drop. TIER does not record practice changes, so the note is where another team can find and copy one.

Monthly close metadata. Closed reports show their actual UTC closed_at, frozen policy and capture-loss disclosure. Late activity evidence does not change closed figures; paid-layer revisions and identity-removing erasure/retention pooling are the supported exceptions, with pooling conserving totals. Scores, comparisons and manifests no longer serve legacy seal timing headers or outcome-dependent revert-window observations. See the monthly metadata contract.

What can I coach from the card?

TIER is built for coaching, not for evaluating developers. Per-person figures are not accurate enough to judge an individual.

Open Developer card with a subject identifier and the credential issued for that subject. GET /api/v1/developer/{id}/coaching and the existing named score route return the same card. Shared read tokens are retired; a teammate without an authorized node/root key cannot open another person's card. Admin reads are named and logged before the card is built; a failed log insert returns 503 with Retry-After. The developer and root receive retained view-log entries; another person's node-key viewer receives none. Two successive reads add two different log entries. Records are filtered to the node-key viewer's dated branch, so the same data snapshot can produce different card contents for different viewers.

Root reads everyone and node keys read their dated branch when names_visibility permits; a personal key retains its holder's card under every setting. See the holder and view-log rules.

Start with the written practice target, the developer's own trend and its uncertainty. The typical range is the CI of total attributed work-type cost divided by total outcomes in one pool of at least k other eligible contributors, not a distribution of people. Each contributor needs at least 50% attributed spend and three outcomes in that work type. There are no named colleague points.

Below 50% attribution, an information icon and “data insufficient” text replace TIER, its CI and work-type trends. Spend, outcomes, cache-read share, overall premium share and the summary share of spend with no issue still appear. The no-issue lever retains its percentage message, and the dashboard can use that share in its practice advice. Capture fidelity cannot clear this floor. With model data, the docs/chore placeholder becomes an available Model choice tile encouraging a comparison of spend and associated points by model. Without model data, it explains that docs/chore classification is absent from the stored taxonomy.

The practice suggestion uses the biggest normalized deterioration in the own trend, with at least three outcomes and 50% attribution on each side. Check again in two weeks; before/after n and nominal 95% CIs (with unvalidated coverage) describe an observation and never establish causation. See the card retro and playbook.

Each developer ships with their own bound credential. Import the team map before starting vendor pollers. Only bound uploads, mapped vendor-attributed events and server watches under --watch-as can establish census eligibility. Other recorded spend, including legacy/shared/manual spend, remains readable within authorized holder scope without adding a counted person. A “not on this card” exclusion, where present, is a share, never dollars. PR and session evidence links contain identifiers only. Structured notes are deferred with PR8.