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:
- People and keys describes branch administration and read-only access. Personal-key claim codes describes the hand-off to each person.
- A node can pad its own subtree.
Padded people with qualifying bound evidence also lend k to ancestor rows, so
the figures of a neighbouring real sub-k group can publish in an ancestor or fold.
Anyone who can read that ancestor and knows the invented amounts could subtract
them to learn the remainder. Monthly comparisons use k and
complementary suppression without person-share or tree dominance gates
(code:
internal/scoring/treesuppress.go,SuppressTree). Read this before treating k as evidence of distinct people. - One server per developer explains how to avoid counting the same work through local and federated paths.
- Federated totals are reported by them, not re-checked here explains what central accepts from another server.
- The child upstream SSRF residual belongs in the host operator's network review.
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.
-
The server address. The web address your TIER server answers on, for example
https://tier.example.com. If TIER runs on your own laptop, it ishttp://127.0.0.1:8080. Open that address in a browser to see the dashboard. Set it once in each terminal window you use:export TIER_HOST=https://tier.example.com # your server's address, no trailing slash -
The root key. Issued on the host with
tierd tokens issue --root; it administers the server. Store its shown-once secret privately. -
Read-only node keys. Issued to a holder for their dated branch. They read that branch and cannot write. References below to a read-token file mean your own issued node key, never a shared password.
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.
-
Start one reachable server. Inputs: issued holder keys (the README upload-token recipe explains the send-only one); set
TIER_HOSTto 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/32servespeaks 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-cidrlets 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. -
Load the team roster. Export a CSV with header
developer,team,division,org, one row per engineer. Use the same GitHub login fordeveloperas in backfill and shipping;divisionandorgmay be empty. Import through the running server with a root key:tierd hierarchy import --server "$TIER_HOST" --api-token @"$HOME/.tier/api-token" \ roster.csvName-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-asand 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. -
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
servefirst and restarting it after all repositories are loaded: backfill writes the same database directly. Keep--sinceat 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 withserve --webhook-secret @fileand send GitHub deliveries toPOST /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). -
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 --uploadDeliver 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_HOSTis the server's HTTPS base address and~/src/appis 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,--opencodeor--museonly for tools that engineer uses; those collectors are off by default. Repeat--repofor 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--sinceforward over time. Re-sends deduplicate. Check exit codes and the per-repository shipping summaries. -
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.
-
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 doctoror 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
developeris the canonical identifier, the GitHub login from step 1. An alias also works; the server resolves it when you load.teamis required.divisionandorgare optional levels in the imported organization path. For an org-level invoice (what the organisation paid a provider, sent toPOST /api/v1/org_actual_spend; see the README's API reference),orgmust match that invoice'sorgexactly. An empty cell is the same as leaving the field out: on re-import, it closes the person's open org memberships at the current UTC month, so those seats receive no org-level invoice allocation for later months. Keep the existingorgwhen updating a team unless you mean to end the membership.- Each
developer,team,division, andorgvalue can be up to 256 UTF-8 bytes after leading and trailing whitespace is trimmed from CSV cells. Names are case-sensitive, soPlatformandplatformare two different teams. - The header must contain these four columns, in any order, with an optional
rejoincolumn.truere-enrols the person in the org from this UTC month after an explicit departure.falseor an empty cell keeps an explicit departure in place, unless a later membership started at or after it ended; then the import reopens the org as usual.rejoinacceptstrue,false, or an empty cell, case-insensitively; any other value is refused. Any other column is refused, so a name column cannot slip in.
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):
- Spend: every Claude Code message is recorded with its git branch. A Codex CLI
session is recorded with the branch it started on, for every message in it. TIER
first looks for a commit on that branch, made within 30 minutes either side of
the message, and takes the issue from the branch names attached to that commit
first, then from a closing directive (
closes #42) or a configured tracker key (OP is built in) such as Jira'sTIER-99in its subject. A bare#42in the subject does not attach spend to an issue. If no matching commit supplies an issue, it reads the issue from the recorded branch name:feature/42-loginbecomes issue 42,fix/TIER-99-crashbecomesTIER-99only when TIER is configured as a tracker key (--tracker-keys TIER). Only the first branch slot counts; later numbers, dates and excluded release, bot and revert branches do not supply an issue. Commits are matched on the last part of the branch name, sofeature/42-loginand42-logincount as the same branch. - Merged work: a merged pull request is tied to an issue from its branch name
or from its description (
closes #42).
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:
- In the synthetic demo, the dashboard's Attribution coverage banner turns red below 50%, and the panel under it breaks the unlinked spend down by the labels above. The panel is available only in the synthetic demo today; the v0.7.0 real-data coaching card ships with raw-data retention. Its attribution line uses a neutral “data insufficient” state below the floor. Name-free monthly comparisons do not publish these per-person attribution diagnostics; use the manifest disclosures and guarded panels.
- In the API,
data_quality.attributed_cost_shareis the linked fraction,data_quality.unattributed_bucketsis the breakdown, anddata_quality.exploratory_cost_shareis themainandmastershare on its own; all are shares of the window's list-price cost. The last two appear in the synthetic demo today, and are left out when the window has no unattributed spend. The v0.7.0 real-data coaching card ships with raw-data retention. It uses its own attribution fields and a coarse excluded share. Monthly comparisons use their manifest disclosures and guarded panels rather thandata_quality(see api-compatibility.md). - A developer can check their own machine with
tierd doctor --repo ~/src/your-repo(the path to their checkout). It reads the last 7 days; in the synthetic demo the banner reads the dashboard's window, 30 days unless you change the From date. Its "issue attribution" line reports how much feature-branch spend is tied to an issue. That is a different share from the banner's, which is taken over all spend. It fails below 50%, warns between 50% and 90%, and passes at 90% or more. Change the failure line with--min-attribution, which takes a fraction between 0 and 1 (--min-attribution 0.4for 40%); the 90% line does not move. Spend recorded onmainormaster, on a detached HEAD, or with no branch is left out of that figure and reported on a separate line, "spend not on a feature branch".
What raises attribution:
- 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. - Start the AI session in the checkout that is on the issue branch: switch
branches there first, or
cdinto that branch's worktree before you start. A session started in the main checkout keeps recordingmainfor work it does in another worktree, as described above. - Put
closes #42in the pull request description, so the merged work is linked too. - Decide how much work on
mainyou 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:
-
Hold a team review using
/api/v1/scores?period=YYYY-MM. Monthly comparisons publishgroup_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 includingmain, 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. -
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.
-
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_HOSTpointing at that demo. The demo's merges sit 7 to 19 days before the day it started, so with that day asS, replaceBEFORE_STARTwithSminus 22 days,CHANGE_DATEwithSminus 14 days andAFTER_ENDwithSminus 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}' doneEach 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, soCHANGE_DATE,AFTER_ENDcovers 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
comparearray contains two monthly manifests, each with its own frozen close, disclosures and tree decision. Match group IDs throughgroup_labelsand read onlyshownrows.rankedmust 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; ifdisclosures.horizon.window_predates_cost_captureis true, choose a month beginning at or aftercost_coverage_safe_sincein 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.
-
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.