Skip to content

Work attribution

spendguard doesn't just cap spend — it answers "what did all this spend and work go toward?" It folds every channel (Claude Code, claude.ai chat, raw LLM API calls, rented GPUs) into one org → team × project attribution, so a person, a team, or an org can see both the hard dollars and the plan-covered value of the work, broken down the same way at every level.

Generic by design

Nothing here is hardcoded to a particular company, project, or team. The taxonomy is discovered from your own corpus and confirmed by you; every name comes from your config or the server. The examples below use placeholder names.


The reporting model

Spend is reported in distinct categories that are never lumped together — hard dollars and estimated value are kept separate, and value is always qualified (it's "est chat value", not bare "value", because chat and Claude Code are covered by your subscription, not billed per token):

# Category Kind Source
LLM API costs hard $ metered per provider × model
Remote compute hard $ per provider × machine type (e.g. vast.ai GPU)
Est chat value plan-covered estimate claude.ai / desktop chat
Est code-chat value plan-covered estimate Claude Code sessions
Cowork placeholder (reserved)
Infra / storage egress hard $ e.g. B2

A subscription line (per-seat Max + Pro) sits alongside as the fixed cost behind ③/④. Periods are day / week / month / quarter / ytd, and the same breakdown renders at org, team, and user scope.

Don't mix period grains

A daily point and a weekly/monthly point are never shown on the same axis — that's the fastest way to make a chart lie. Pick one period per view.


Where the data comes from

flowchart LR
  cc[Claude Code<br/>~/.claude transcripts] --> A
  chat[claude.ai chat<br/>opt-in, on-device] --> A
  api[LLM API calls<br/>the gate ledger] --> A
  gpu[vast.ai GPU<br/>snapshots] --> A
  A[attribution.py<br/>org → team × project] --> roll[day rollups]
  roll --> saas[(team roll-up<br/>scrubbed aggregates)]
Command What it attributes
spendguard claude-code [show\|sync\|classify\|work\|story] mines ~/.claude transcripts → Claude Code spend + work, classified per session
spendguard chat [test\|show\|discover\|classify\|loop\|work\|story\|sync\|status\|accept] the claude.ai chat adapter (opt-in, on-device, macOS)
spendguard resources [show\|snapshot\|sync] vast.ai GPU → org/team/project; snapshot records instances so destroyed ones stay reconstructable
spendguard schedule [--daily] [--remove] installs the OS-native scheduler that keeps all of the above synced

The taxonomy is discovered, then confirmed

Attribution uses contextual reasoning over the content of each day's work, not brittle path/name rules — because one person can have many orgs, and one org many projects, and a single conversation can touch several projects at once.

spendguard chat discover     # reads your corpus, PROPOSES an `org → team × project` taxonomy
                             #   (seeded from your current one; prints a diff for periodic review)
spendguard chat classify     # assigns each conversation {org, team, allocation:[{project, pct}]}

Two dimensions, kept orthogonal:

  • org → team is the additive scope tree — every dollar rolls up exactly once.
  • project[] is the orthogonal, multi-valued dimension — a conversation's value splits across the projects it actually touched (allocation percentages that sum to 1.0), so a cross-project session is counted once but visible under each project.

Both discover and classify run under the caged advisor budget (a hard meta-cap), and classify without --run is an estimate-only dry run.


Consistent classification across a team

When you connect a team roll-up, the taxonomy lives on the server (versioned org_taxonomy). Every member pulls the same taxonomy before classifying, so the whole org buckets work consistently instead of each person inventing their own labels:

spendguard saas pull-taxonomy     # fetch the org's shared taxonomy
spendguard saas push-taxonomy     # propose an update (admins)

From the dashboard, an admin can press "Request attribution" — that enqueues an attribute command the clients pick up on their next saas sync and run locally (on-device; only scrubbed rollups are pushed back).


GPU attribution without false positives

vast.ai exposes no per-day consumption or audit log, so spendguard records it itself: every saas sync takes a resources.snapshot(), and per-day cost is reconstructed from instance start/end × hourly rate (so a short-lived box destroyed before the next sync is still counted).

Attribution maps an instance to a project via config (resources.vastai.label_map) and, for anything still unlabeled, by aligning to conversations active that day — the same cross-connect the LLM-batch attribution uses — but restricted to GPU-capable projects/teams so an unrelated chat never accidentally claims a GPU bill. There is no opinionated default label map: until you configure one, an unlabeled instance maps to no project rather than a wrong one.


Keeping it current

spendguard schedule --daily        # install (macOS launchd · Linux cron · Windows schtasks)
spendguard schedule --remove       # uninstall

The scheduler runs spendguard saas sync --if-due, which snapshots GPUs, runs the chat loop (if enabled), and pushes the day's scrubbed rollups. It's idempotent and dependency-free.

Privacy & opt-in

The chat adapter is off by default (chat.enabled), runs entirely on your device, decrypts your own session cookie locally, and never logs, prints, or transmits it. As with the rest of spendguard, only scrubbed aggregates (daily totals + generalizable abstracts) ever leave your machine, and only when you choose to sync.