TIER docs

TIER for engineering managers: common questions

TIER (Token Impact & Efficiency Ratio) measures how much merged work a team gets for its AI spend. 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 below also cover the legacy shared-token workflow. Those tokens do not become organization sign-in keys on upgrade.

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 shared/legacy keys. It links who can create people, issue keys and read data.

Before inviting a team, read these limits:

Being above someone in the tree does not by itself open their card. Use the declared-viewer rule for individual figures. 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. In developer mode, a developer's page is visible to that developer and to the viewers the operator configures: the team's declared manager, an optional skip-level viewer, or whoever a works agreement names. Teammates never see it. Every viewer sees the same page, and the developer can read the log of who viewed it, including administrators. Managers may coach from it; no one may ask for figures beyond it. TIER is not suitable for pay, promotion, appraisal, reviews, improvement plans, discipline or dismissal. 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.

Identity batch (#1370): Real-data developer startup requires an active permitted viewer credential, a nonempty admin token and a named audited operator. Shared reads are name-free; named card reads use verified provenance and log each view before building it. The coaching card replaces named detail and requires a configured subject viewer credential or a live declared-viewer check, plus positive verified spend. The #1371 raw-data retention engine is built; identity, retention and the card ship together in v0.7.0. See developer access and upload identity for current commands and restrictions.

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: Verified coaching cards are available to the subject and permitted viewers with audited reads and raw-data retention; shared reports stay name-free. 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.

On the dashboard, if the server needs a token, a password box appears next to the Refresh button. Paste the read-only token there and click Refresh. The page keeps it until you close the browser tab.

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. With sign-in turned off, tierd serve is restricted to loopback IP addresses (such as 127.0.0.1 or ::1, without a zone suffix) or the exact hostname localhost; any other address needs an admin token. If a request with no token works instead of failing with HTTP 401, sign-in is off: drop the printf line and the -H @-.

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. New in v0.6.2, two experimental, 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 team report requires a sealed calendar month (step 6).

v0.7.0 uploads: Give each laptop its developer's bound upload credential for POST /api/v1/events. Legacy shared ingest remains an administrator-trust boundary: its holder can submit spend under other IDs, but those rows stay team-only and 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.

From v0.7.0, identity batch #1370 adds each developer's bound credential (tierd ship --bound-token @file --developer ID); shared ingest stays team-only. Real developer mode requires subject-bound viewers and a named audited operator; named detail uses verified spend and logs each read before construction. The coaching card (#1372) and raw-data retention (#1371) ship together in v0.7.0; the separate per-issue coaching report remains future work. The developer, declared manager, optional skip-level and works-agreement viewers see the same card; teammates cannot open it. The developer can see who viewed it, including administrators. Practice levers come first, TIER last; premium-model and unattributed shares stay per-person, while team rows publish only cache_read_share. See identity access. TIER is built for coaching, not for evaluating developers. Per-person figures are not accurate enough to judge an individual. Agree to coach the practice, never rank people (peer learning).

Install tierd via quickstart. On the server, create three different tokens, one per file, with the README recipes: (umask 077; openssl rand -hex 32 > ~/.tier/api-token), then the same for read-token and ingest-token (serve refuses two equal tokens). Replace the example inputs.

  1. Start one reachable server. Inputs: the three token files above (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 --aggregation team --addr 127.0.0.1:8080 --db "$HOME/.tier/tier.db" \
      --api-token @"$HOME/.tier/api-token" --read-token @"$HOME/.tier/read-token" \
      --ingest-token @"$HOME/.tier/ingest-token" --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 the admin token:

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

    A group is shown only when it has at least k counted people (default 5, minimum 3) and every figure it would publish (captured spend, merged-work points and, where present, paid spend and capture shares) is carried by at least k of them. They need not be the same people for each figure. Otherwise the group folds into other; if other also falls short, the response is withheld. The roster must be loaded before sealing; a sealed report does not change after an import.

  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 2026-09-17 \
      --token @"$HOME/.tier/github-token" --db "$HOME/.tier/tier.db"
    

    Run this on the server machine, stopping serve first and restarting it after: backfill writes the same database directly. Repeat for each repository. 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" --from-team-map \
      --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 or sealing. Each key is bound to its subject; a shared ingest token supplies 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 2026-09-17 \
      --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":"2026-09","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. Seal the first calendar month. Results come from a SEALED calendar month; a month returns 404 until it is sealed. Finish loading cost, merged work, roster and any identity aliases before arming: the first seal pins the earliest published month permanently, and sealed reports never change. Follow these steps on the server before the irreversible confirmation:

    1. Establish the dated roster and finish the uploads and aliases. Roster placement cannot be backdated: history before a person's first assignment stays in other, and unrostered people do not count toward k. If a populated other falls short of a contributor floor, the whole report is withheld.

    2. Leave serve running to register its sources. Preview with the same database, configuration, environment, prices, k and grace settings:

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

      --arm earliest selects by cost coverage, not roster coverage. Check the printed month against the dated roster and retained laptop uploads. The first readable month must be a full UTC month in which the roster was already effective for at least k counted contributors. If the preview predates the roster, choose a later eligible --arm YYYY-MM in both commands. Importing a roster today cannot qualify an earlier month. The dry run writes nothing and does not warn about a fully withheld census.

    3. Confirm only after that check, preserving the preview's month and settings:

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

    The confirmation command prompts and permanently seals that month. It needs a full UTC calendar month of cost and roster coverage, after it closes plus the default 14-day grace (--report-grace), once configured sources have settled through its end and the seal succeeds. With the roster already effective for k contributors by October 1 and cost coverage starting 2026-09-17, October is the first eligible full month; it becomes sealable on 2026-11-15 at 00:00 UTC, not when September's backfill finishes. If the retained cost horizon starts later, so does the first full month. With no eligible history, wait for a full covered month and its grace or backfill available history. See the sealing procedure for source gates, custom settings and sealing status. Later eligible months seal at writable-server startup and on its hourly pass.

Then read the team report for a sealed month, using the read token (replace 2026-10 with the month actually sealed):

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, enter the read token and select the same Sealed 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?

Yes, through v0.7.0 coaching cards: developers and their explicitly declared viewers can read that subject's verified coaching card with subject-bound credentials; shared reads stay name-free. The card ships with raw-data retention; the separate per-issue report remains future work. See identity access. tierd serve requires --aggregation, with no default (quickstart.md); the modes below describe v0.7.0:

Mode What reports show
developer Subject-bound verified detail and audited reads; shared /scores has no named rows. Named reads return the coaching card.
team Teams only. No individual appears in any report.
division Divisions only, one level above teams. No individual appears.

From v0.7.0, #1370 named-viewer controls determine which subject a credential can read; shared read-only reports stay name-free. The legacy shared read-only token is server-wide. Organization node keys scope sealed reports to their subtree; they do not open named cards merely by being above someone in the tree. The synthetic demo shows every invented developer's row. GET /api/v1/events exposes each recorded AI message's time, model, repository, issue and session id, per developer. From v0.7.0, this raw bulk route is denied on real developer data, including admin reporting reads; compliance exports are separately audited. In team and division mode, /api/v1/events returns HTTP 403. 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?).

Built in #1370. Each developer's verified detail opens only to that developer, their declared manager, an optional skip-level viewer, or declared works-agreement viewers. Team membership never grants access. The developer can read who viewed their data, including administrators. The shared token keeps only name-free responses. See identity access for commands.

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 developer mode, aged spend and merged-work rows are pruned into a name-free roll-up; the permanent cutoff applies in every mode afterwards. 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).

In team and division mode there is 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. TIER counts people, not identifiers (#856). An identifier with spend or merged work in the window you asked for counts toward k only if all of these hold:

Spend recorded under unattributed is left out of every figure (see below). Any other identifier that does not count keeps its spend and merged work in its group's figures; it just does not make the group bigger. On top of that, each figure needs its own k: a group's cost, when non-zero, is shown only if at least k counted people in it have captured spend (hand-entered spend does not count here either), its merged-work points, when non-zero, only if at least k have merged work, and its paid spend (actual_paid_usd), when non-zero, only if at least k have paid spend. So five people where only one has spend, or only one has an invoice, is not shown. tier and cost_per_point need cost and points; spend_leverage needs cost and paid spend. coverage_pct splits cost into per-request and daily or hand-entered parts, and each part that is not zero needs k counted people with captured spend behind it.

This means more groups are withheld than before #856. The response says how many identifiers did not count, and why, in data_quality.uncounted_active_ids (manual_only, push_only, not_on_roster, bot), once for the whole window. To bring those people back into the count, capture their work through a collector or the proxy and put them on the roster. This protects what someone holding the read-only token can see. Anyone holding the admin token can still edit the roster and aliases, so treat that token as an admin credential (#908). A group smaller than k is folded into a row called other. That row also holds everyone with no team (in division mode, no division) and any team you named other, and it does not say which is which. If other is itself smaller than k, TIER withholds the whole response: every team row, other and the company total. It says so in data_quality.kanon_suppressed, an object that gives how many counted people were withheld and the k in force. A group can be withheld with k or more people in it when one of its figures is carried by fewer than k of them. The named rows go too because another view that folds one of those teams into its own other row (a period comparison does) would let anyone subtract the team's row and read the hidden group's numbers. In these modes each report is one calendar month (?period=YYYY-MM), and whether a month's rows are shown is decided once, when the month is sealed. They are shown if other then holds at least k counted people (people with no team, or in a team too small for its own row) and every figure it shows is carried by at least k of them, or if no identifier in it has any spend or merged work. Until it is sealed the month is not served at all (a 404, carrying sealable_at when the month will become sealable); once sealed it never changes, so a withheld month stays withheld, and there is no wider window to ask for (#913; see the API changelog). Putting the people in other on teams does not help unless each of those teams reaches k on its own.

In these modes the response shows one breakdown of the window (#864): the team (or division) rows and the company total. It leaves out the per-work-type work_types view, the spend breakdown cost_composition (by model, by token type, linked and unlinked), and the attribution figures unattributed_buckets and exploratory_cost_share, and it refuses ?work_type=. It does not show attributed_cost_share: beside the team rows and the total it could reveal one person's spend. Spend TIER cannot tie to any person (an org-usage poller's remainder, proxy requests with no developer header) is left out of every figure in these modes; see excludes_unattributed_spend in api-compatibility.md. A second breakdown can be subtracted from the first: team totals minus work-type totals recovered a 3-person group's spend in testing, and a model only one developer used showed that developer's spend in the model breakdown. Issue #937 tracks an audit that would let those views come back. The per-developer endpoint GET /api/v1/scores/{developer} answers not found (HTTP 404) for every name in these modes. In the synthetic demo it returns that invented developer's score; real-data per-developer detail requires #1370 subject-bound credentials and verified spend.

Every mode stores per-developer identifiers in the database. The built coaching card displays a subject identifier; release is gated on retention. On the spend side it is the developer's OS username (what whoami prints on their machine), unless they send spend with tierd ship --developer <name>. Two people with the same username on different machines would share one identifier, so give one of them a different --developer. On the merged-work side it is the GitHub login of the pull request's author. When those differ, map them once (next section) so each person is counted once in group reports and one subject on the built coaching card, whose release is gated on retention.

To see which mode a server is running:

printf 'Authorization: Bearer %s\n' "$(cat ~/.tier/read-token)" |
  curl -fsS -H @- "$TIER_HOST/api/v1/scores" |
  jq '{error, aggregation, developers: (.developers | length)}'

An error that is not null means the request failed; read it before the other two fields. In v0.7.0, "team" and "division" identify group modes. Real developer-mode shared reads return "aggregation": "developer", "identity_scoped": true and an empty developers array. Their total can be withheld by the viewer-wide differencing guard. Named reads use the audited coaching-card route. The synthetic demo uses "aggregation": null; a developers count of 0 means no invented data in the API's default 90-day window. "team" or "division" means names are off; in those modes the developers count is always 0, because no individual rows are sent. If the command prints nothing, read curl's error line: the request failed. On a team or division server a 404 there means no month is sealed yet (or sealing is not armed); run it with --fail-with-body in place of -f (curl -sS --fail-with-body) and jq prints the 404's error and aggregation.

Team mode exists for organisations where employee-representation rules, such as works councils in Germany, France and the Netherlands, or data-protection law such as GDPR, may restrict measuring named employees. legal-and-privacy.md sets out that reasoning, a table of recommended modes by country, and the consultation steps before go-live. It is not legal advice, and neither is this page. Two things to know before you decide:

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 (all of these need the admin token):

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 OS username, which the developer can get by running whoami on their machine. 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 recomputes a score or creates a new seal, because it joins identities when computing scores. Already-sealed team/division months stay as sealed: they return stored results and are not recomputed. An alias does not change which team that earlier spend counts toward (see below).

If you skip this, that person shows up as two identifiers: one with spend and no merged work, the other with merged work and no spend, and neither score means anything. GET /api/v1/scores lists identifiers found on only one side under data_quality.unjoined_developers (team and division reports show only counts; the synthetic demo shows invented identifiers). A real-data per-identifier view requires subject-bound credentials and verified spend. The coaching card is built; release is gated on retention. Missing merged work may simply mean the person has not merged anything in the window yet, so check your 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 the admin token:

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 in other 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 other (the no-team row) 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?

In team or division mode, they are the main view. The dashboard shows one row per team (or division), and GET /api/v1/scores returns them in the teams array. Teams under the k floor appear inside other.

In these modes, choose a sealed calendar month with ?period=YYYY-MM; leave the parameter out to read the latest sealed month. Arbitrary since= and until= windows are available only in the synthetic demo; real-data developer mode requires #1370 subject-bound credentials. 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 -c '{predates: .data_quality.window_predates_cost_capture},
    ((.teams // [])[] | {team, tier, weighted_points, total_cost_usd, ranked})'

The first line printed is about the month. predates: true means the month starts before TIER began recording spend: merged work from before that point counts but its spend does not, so the rows can read too high. Pick a later sealed month. predates is null when TIER had no coverage start to report — no spend recorded yet, or the server could not read it. Each line after it is one team (or division); if none follow, the month has no data or its rows were withheld (see data_quality.kanon_suppressed above). A sealed month carries no data_quality.cost_coverage_safe_since. If curl reports a 404, that month is not sealed; run the curl part with -sS --fail-with-body in place of -fsS, with the | jq stage left off, to read the reason.

For one team, select its row by the name exactly as you loaded it; in division mode, use the division name instead:

printf 'Authorization: Bearer %s\n' "$(cat ~/.tier/read-token)" |
  curl -fsS -G -H @- "$TIER_HOST/api/v1/scores" --data-urlencode "period=2026-09" |
  jq '{row: ((.teams // [])[] | select(.team == "platform")), predates: .data_quality.window_predates_cost_capture}'

The row holds that team's tier, weighted_points, total_cost_usd (at list price), cost_per_point and ranked. If the team has no named row that month, jq prints nothing: it may have no data, be folded into other, or be withheld. predates means the same as above.

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 splits remain a synthetic-demo example; real-data developer mode uses the name-free total guard 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.

In team and division mode ?team= is refused with a 400. It rolls up a single team with no minimum group size, so honouring it there would expose a one-person team. In the synthetic demo every invented developer is already named, so it reveals nothing new. Real-data developer mode requires #1370 subject-bound credentials.

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 issue in the 14 days before it merged. 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?

They don't add up, and they aren't averaged. TIER is weighted points of merged work per $1,000 of AI spend. Each merged outcome 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 and shows beside the score as actual_paid_usd and spend_leverage (list-price cost divided by the amount paid); 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 company total covers every developer in the window, including anyone not assigned to a team. In team or division mode, whenever data_quality.kanon_suppressed is present (see Can I see my developers' names?), the total is withheld along with every team row.

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; team and division reports do not publish it. 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. Those rules have version numbers, stamped on every /api/v1/scores response as rubric.version and price_table.version: the rubric version fixes how merged work turns into points, and the price-table version fixes what each AI model's tokens cost. A change in either moves the number without any change in the work (rubric.md).

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

Not necessarily. "Unattributed" means TIER could not connect that spend to an issue number. It does not mean the work never merged, unless the pull request named no issue; see below.

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 sealed reports.

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

See availability. v0.7.0 provides name-free cache-share levers over sealed team/division months and verified 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 subject-bound viewer credentials 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 sealed 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 in team or division mode. v0.7.0 publishes only teams[].cache_read_share, the share of input-side tokens read from the provider's cache, over sealed months. Each published group needs k counted seats with captured spend and merged work; any failing group withholds all levers. The cache partition also needs k contributors with cache reads and k without, counting distinct cost contributors with positive input-side tokens (input + cache read + cache write), including bots and spend-only people. Zero-input contributors count on neither side; k is never below 3. data_quality.team_levers_withheld and team_levers_withheld_reason explain withholding; 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 retain only cache share; detailed model, token-class and work-type tables stay suppressed. 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, and TIER does not credit merged work to a model, 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). Arbitrary-window team roll-ups are a post-#826 practice on real data. 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.

    In team or division mode the server refuses ?team=, since and until with a 400: it answers one sealed calendar month per call. Compare two months instead, and read the team's row:

    for period in 2026-05 2026-06; do
      printf 'Authorization: Bearer %s\n' "$(cat ~/.tier/read-token)" |
        curl -fsS -G -H @- "$TIER_HOST/api/v1/scores" --data-urlencode "period=$period" |
        jq -c '{row: ((.teams // [])[] | select(.team == "platform") | {tier, ranked}), predates: .data_quality.window_predates_cost_capture}'
    done
    

    Sealed months freeze the recorded observation; they do not align each merge with all of its spending. Two complete months can still have different boundary-crossing work. Before interpreting a change, document differences in repository scope, membership, work mix, capture and measurement rules. If those differences are unknown or substantial, report the two observations without calling their difference an improvement or deterioration. Months differ in length by up to three days, the nearest a sealed read comes to equal periods. A sealed month carries no safe_since; if predates is true, pick a later month. If curl reports a 404, that month is not sealed; --fail-with-body in place of -f, with the | jq stage left off, prints the reason. If curl printed no error and jq printed nothing, there is no platform row that month. To see why, run the same curl with jq -c '{teams: [(.teams // [])[].team], total: (.total != null), suppressed: .data_quality.kanon_suppressed}'. If teams lists other, platform may have had fewer than k counted developers that month (or none at all), or a non-zero measure with fewer than k counted contributors; anything it had is folded into other. If suppressed is not null, figures were withheld, as described in Can I see my developers' names?. If teams is [], total is false and suppressed is null, the month has no data at all. In division mode the row's team field holds the division name.

    Compare the two tier values as a direction, not a measured effect: team roll-ups carry no bootstrap bounds. Check that ranked is true in both periods; if either is false, there is too little evidence to read the change. If predates is true for the earlier period, it started before TIER was recording spend: its merged work counts but the spend before that point does not, so its score reads too high. In the synthetic demo only, pick a start date on or after the safe_since date the command prints (data_quality.cost_coverage_safe_since); it is null when no coverage horizon was available, including when no spend was recorded or the coverage lookup failed. GET /api/v1/scores/compare does not help here. It covers the whole company with no team filter, and its total and teams rows always say significant: false. Its per-developer rows retain a raw unvalidated non-overlap field; neither value establishes an inference. The dashboard shows “inference not established”. TIER is built for coaching, not for evaluating developers. Per-person figures are not accurate enough to judge an individual.

  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.

Seal timing disclosure. Sealed reports show the actual UTC seal timestamp, configured grace (or “not recorded” for legacy seals) and elapsed days after period end. In team and division mode, revert-window status is withheld because outcome-dependent observations can identify a person in an anonymized cohort. Only developer-level disclosures can state that some outcomes' 60-day revert windows were still open, when recorded as true at sealing. The default 14-day grace can leave those windows open; longer grace does not establish delivery completeness. Late evidence never changes sealed bytes. See the seal disclosure wire 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 and teammates cannot open it. Admin reads are named and logged before the card is built; a failed log insert returns 503 with Retry-After. The card shows every retained view to every permitted viewer. Two successive reads add two different log entries; with the same data snapshot, the builder's bytes are identical under different viewer contexts.

Declared viewers may also use their own node or personal sign-in key after a live declaration check; being above someone in the tree grants no card access. See the declared-viewer and view-log rules. For the legacy shared admin token, see the evaluation-mode access choice.

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 all work-type and no-issue figures. Spend, outcomes, cache-read share and overall premium share still appear. Capture fidelity cannot clear this floor. The docs/chore tile explicitly withholds figures because those categories are 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 contribute. Legacy/shared/manual spend is team-only; “not on this card” is a share, never dollars. PR and session evidence links contain identifiers only. Structured notes are deferred with PR8.