Docs

Tallyhook documentation

Tallyhook is a cost ledger for teams using AI coding agents. A collector on each developer's machine reads the session logs Claude Code and Codex CLI already keep, and the web app turns them into cost per client, repo and developer, with invoice output.

Updated

Quick start

  1. Sign in with GitHub. A workspace is created and the 14-day trial starts; no card needed.
  2. On the Install page, create a collector token and copy the command. Run it on each developer's machine (macOS or Linux with Node.js 18+):
    curl -fsSL https://tallyhook.dev/install.sh | sh -s -- th_your_token
    It downloads one file to ~/.tallyhook/tallyhook.js, registers Claude Code SessionEnd and Stop hooks in ~/.claude/settings.json, and uploads existing history. The Install page flips to "Receiving" the moment the first upload lands.
  3. On Clients, create your clients and pick one for each repo. Every past and future session from that repo is attributed to that client.
  4. Open Invoice for a client and month to get billable lines with your markup, download CSV, or share a read-only report link.

Prefer not to pipe to a shell? Use the open-source npm package instead: npx tallyhook install th_your_token. Or download and inspect the file first: curl -fsSLo tallyhook.js https://tallyhook.dev/tallyhook.js, then node tallyhook.js install th_your_token.

What gets uploaded

The collector is a single dependency-free Node.js file of about 1,019 lines, MIT-licensed (source on GitHub, package on npm, or read the served file). It parses ~/.claude/projects/**/*.jsonl (Claude Code) and ~/.codex/sessions/** (Codex CLI, including the .jsonl.zst files Codex compresses after seven days, on Node 22.15 or newer). Per session it uploads:

  • Tool (Claude Code or Codex), CLI version, entry point, start and end time
  • Repo (from git remote.origin.url), branch and folder name
  • Developer identity from git config --global user.email and user.name (or your OS username and hostname if no git email is set), plus the machine hostname
  • Tokens by model: input, output, cache reads, 5-minute and 1-hour cache writes, request count, and output tokens written by subagents
  • Turn count, tool-call counts by tool name, and the paths of files edited (relative to the session's working folder, or just the file name for files outside it; at most 200)
  • The first 160 characters of the first prompt, unless privacy is on

Never uploaded: transcripts, later prompts, responses, code, diffs, file contents, tool output, command results, environment variables or secrets. Only files that changed since the last sync are read again.

How sessions are attributed to clients

  • Each session's repo is its git remote, normalized to host/org/repo in lower case, so git@github.com:Acme/Web.git and https://github.com/acme/web are the same repo. A folder without a remote becomes local:<folder>.
  • A repo rule maps one repo to one client. Setting or changing it re-tags every past session from that repo and applies to all future ones.
  • Sessions from repos without a rule are Unassigned. Overview lists those repos with their cost so nothing is silently absorbed.
  • Internal work can be its own client (for example "Internal"), so the whole bill is accounted for.

How cost is calculated

Each session is priced at the vendor's public list price for each model it used (current table):

cost = ( input × input_price
       + output × output_price
       + cache_read × cache_read_price
       + cache_write_5m × 1.25 × input_price
       + cache_write_1h × 2 × input_price ) / 1,000,000
  • Claude Code writes one log row per streamed content block, each repeating the message's usage. Tallyhook counts each message.id once per session, across the session file and its subagent files (forked subagents copy parent messages), keeping the most complete copy.
  • Codex CLI logs a running token total per session; Tallyhook uses the final total, with cached input priced at the cache-read rate.
  • Overrides: owners can set a price for any model in Settings (for negotiated rates or a model not yet in the table). Every session is repriced immediately.
  • Unpriced models (including unlisted variants such as a new -mini) are flagged in Settings and on each session instead of being silently counted as $0 or at another model's price.
  • Fast mode on Claude Opus is priced at its premium rate. Not included: server-tool fees such as Claude web search ($10 per 1,000 searches), the 1.1x rate for US-only inference, OpenAI long-context surcharges, and a Codex session that switches models is priced at its last model.
  • Subscriptions: on Claude or ChatGPT subscription seats there is no per-token bill, so the figure is a list-price equivalent: the value of the usage, which is what you bill a client against. The app labels it that way everywhere.

Invoices, exports and client reports

  • Invoice view: per client (or all clients) for a date range, with lines by developer and by model, list cost, and billable total with the workspace markup applied. Printable.
  • CSV exports: invoice lines or every session, per client and date range, ready for Xero, QuickBooks or a spreadsheet. Cells that start with = + - @ are escaped against spreadsheet formula injection.
  • Client report links: a read-only, no-login page for one client, revocable at any time. It can only ever show that client.
  • Markup: a percentage per workspace, set by an owner in Settings, and optionally a different percentage on any single client's page. A client with its own rate is billed at that rate; one with an empty field inherits the workspace rate. When a document covers several clients at different rates, each client line is priced at its own rate and the per-developer and per-model billable columns are dropped, because no single markup would describe them.
  • Invoice details: a billing name and address, a tax or VAT number, and payment terms, all set in Settings and printed on the document. Amounts stay in USD: the list prices they are derived from are published in USD, and converting them would mean inventing an exchange rate the figure could not be checked against.
  • Billing timezone: an owner sets the workspace timezone in Settings and every calendar-month range (“This month”, “Last month”, and a custom range's start and end days) is cut on that clock rather than on UTC. This is what stops a session run at 8pm on the last day of the month from being billed in the next one, since in UTC it is already tomorrow. Sessions are stored in UTC either way and nothing is repriced; only the period boundaries move. Per-day bars in the charts still group on the UTC date.

Alerts, budgets and forecast

  • Cost alerts: set a dollar threshold in Settings. The first time a session's cost reaches it (checked on every sync, so a long session can trigger while still running), owners get an email. Each session alerts once.
  • Client budgets: set a monthly budget (list cost, calendar month) on any client's page. Owners get one email when that client's month passes 80% of it and one when it passes 100%. The Clients page shows how much of each budget is used.
  • Weekly summary: on Mondays, owners get last week's cost by client, the most expensive session, budget status and the month-end pace. Turn it off in Settings.
  • Month-end forecast: Overview projects the month's total from the pace so far.
  • Activity: a log of token, client, member and billing changes.

Webhooks

An owner can add a webhook URL in Settings. Both alerts above are then POSTed to it as well as emailed. The body is { text, content, event }: content is what Discord reads, and a real alert has been confirmed rendering correctly in a Discord channel; text is the equivalent field Slack documents, though no message has been watched arriving there; event carries the structured detail (kind is cost_spike, budget or test). Saving the URL sends a test immediately and tells you whether it arrived. The URL must be https and cannot point at a private or loopback address, because the server is the one making the request. Redirects are not followed, the request times out after five seconds, and a failing webhook never affects an upload or an email.

Team, roles and seats

  • Invite teammates with a link from Team. Everyone signs in with GitHub.
  • Members see all workspace data, manage clients and repo rules, and create or revoke their own collector tokens.
  • Owners also manage billing, markup, price overrides, members and the invite link. A workspace always has at least one owner.
  • A seat is a developer identity that uploads sessions. Members who don't run the collector are free (up to 50 people per workspace). The trial covers the developer count of whichever plan you are trying (5 on Team, 20 on Agency); switch between them free in Settings while the trial runs. When the seat limit is reached, a new developer's upload is refused with a clear message until you upgrade or add seats.

Collector CLI reference

After install the collector lives at ~/.tallyhook/tallyhook.js. Every command below also works as npx tallyhook <command>.

node ~/.tallyhook/tallyhook.js install <token> [--api URL]   # save config, register hooks, full upload
node ~/.tallyhook/tallyhook.js sync [--full] [--dry-run] [--quiet]
node ~/.tallyhook/tallyhook.js status                         # api, token suffix, last sync, developer identity
node ~/.tallyhook/tallyhook.js uninstall                      # remove hooks and ~/.tallyhook
npx tallyhook [--days 30] [--by repo|model|client] [--json]  # local only: what each repo cost, nothing uploaded, no install needed
npx tallyhook clients                                        # local only: the billing map, and repos no client claims yet
npx tallyhook clients add "<name>" <repo-pattern>... [--rate 20]
npx tallyhook clients rm "<name>" | markup <pct>
npx tallyhook mcp                                            # local only: MCP server over stdio, no account, no token

Try it without an account: npx tallyhook with no arguments prints what each repo on this machine cost over the last 30 days at list price. It needs no token and uploads nothing; the only network call fetches the public price list from /api/prices. Its --json output can also be turned into a shareable build-cost card. report still works as a subcommand, so older scripts keep running.

Billing clients, locally: clients add maps repo patterns to a client with a markup, and npx tallyhook --by client then prints cost beside a billable figure. Patterns match case-insensitively as a substring of the repo string (github.com/org/repo, or local:<folder>), and * makes one an anchored glob. The mapping is stored in ~/.tallyhook/clients.json and is never uploaded. A client report covers only mapped repos and states in money what it excluded, so a half-mapped machine produces a visibly partial total rather than a plausible wrong one.

  • mcp starts a Model Context Protocol server over stdio so a coding agent can ask what its own work cost. In Claude Code: claude mcp add tallyhook -- npx -y tallyhook mcp. Four tools: usage_by_repo, usage_by_model, expensive_sessions and usage_summary. Like report it reads the same local logs, needs no account or token, and uploads nothing.
  • sync --dry-run parses the files that changed since the last sync (add --full for all history) and prints totals and a sample session without any network call, so you can check what would be sent.
  • sync --full re-uploads all history; the server replaces each session's totals, so it is safe to repeat. A new collector version does this once automatically, saving progress after each batch.
  • Hooks: SessionEnd syncs when a session closes; Stop syncs at most every ten minutes. Each upload times out after eight seconds, Claude Code stops the hook after 20 seconds at most, and a hook never fails a turn.
  • The collector doesn't register a Codex CLI hook yet, so Codex sessions upload on the next Claude Code hook or a manual sync. If you only use Codex, add node ~/.tallyhook/tallyhook.js sync --quiet to cron.

Configuration

~/.tallyhook/config.json (comments added here for explanation; the file itself is plain JSON):

{
  "token": "th_…",              // workspace collector token
  "api": "https://tallyhook.dev",
  "privacy": false,              // true: never send the first-prompt snippet
  "devEmail": "you@agency.dev",  // optional: override git config identity
  "devName": "Your Name"
}

Ingest API

The collector talks to one endpoint; you can post to it from your own tooling with the same token.

POST https://tallyhook.dev/api/ingest
Authorization: Bearer th_…
Content-Type: application/json

{
  "collector": "0.2.0",
  "dev": { "key": "maya@agency.dev", "name": "Maya", "email": "maya@agency.dev", "machine": "mbp" },
  "sessions": [{
    "tool": "claude-code",            // or "codex"
    "session_id": "…",                // upsert key within the workspace and tool
    "started_at": "2026-09-15T10:00:00Z", "ended_at": "2026-09-15T11:02:00Z",
    "repo": "github.com/acme/web", "branch": "main", "cwd_name": "web",
    "turns": 12, "tool_calls": { "Edit": 9 }, "files_touched": ["src/app.ts"], "files_count": 1,
    "first_prompt": "Fix the login redirect",
    "models": { "claude-opus-5": { "input": 1200, "output": 38000, "cache_read": 940000,
                "cache_write_5m": 50000, "cache_write_1h": 0, "requests": 41 } },
    "subagent_output_tokens": 0
  }]
}
  • Response: { "accepted": 1, "unpriced_models": [] }.
  • Limits: 4 MB body, 200 sessions per request, 60 requests per minute per token, token counts capped per field.
  • Errors: 401 missing or revoked token, 402 seat limit reached, 413 body too large, 429 rate limited, 400 malformed input.
  • The token decides the workspace; nothing in the body can redirect data elsewhere. Tokens are stored hashed.

Troubleshooting

  • Nothing arrives: run node ~/.tallyhook/tallyhook.js sync and read the message. status shows the API URL and last sync.
  • A session shows as unpriced: its model isn't in the price table yet. Set an override in Settings; it applies to past sessions too.
  • Two identities for one developer: set devEmail in the config so every machine reports the same key.
  • Old Codex sessions missing: Codex compresses rollouts after seven days; reading them needs Node 22.15 or newer.
  • Windows: not supported yet (the installer and hooks assume a POSIX shell).

Still stuck? support@tallyhook.dev.