Cost Attribution

Because Stategraph holds your whole fleet's state of record, it can answer the question a per-file cost plugin cannot: who owns this spend, and how is it trending? The tenant endpoints roll up the latest snapshot of every live state and break it down by provider, type, or tag, in a single call.

GET /api/v1/tenants/{tenant_id}/costs[?tag_key=KEY]                    # rollup + breakdowns
GET /api/v1/tenants/{tenant_id}/costs/tag-keys                         # tag keys available to group by
GET /api/v1/tenants/{tenant_id}/costs/history[?from&to&group_by]      # cost over time

Tenant Rollup

stategraph cost tenant --tenant $TENANT_ID
# or, against the API directly:
curl -H "Authorization: Bearer $STATEGRAPH_API_KEY" \
  "http://localhost:8080/api/v1/tenants/$TENANT_ID/costs"

One call returns the tenant total, coverage, a by_provider and by_type breakdown, and a per-state list:

{
  "monthly_cost": "1573.964000",
  "currency": "USD",
  "coverage_percent": 51.3,
  "by_provider": [
    { "name": "aws", "monthly_cost": "1573.964000", "hourly_cost": "2.156116", "resource_count": 221 }
  ],
  "by_type": [
    { "name": "aws_db_instance",        "monthly_cost": "1179.900000", "resource_count": 5 },
    { "name": "aws_elasticache_cluster", "monthly_cost": "360.620000",  "resource_count": 2 },
    { "name": "aws_instance",           "monthly_cost": "31.244000",   "resource_count": 4 }
  ],
  "states": [
    { "state_name": "acme-data-stores", "monthly_cost": "1509.640000", "coverage_percent": 50.0,
      "resource_count": 24, "state_id": "5156028b-…" }
  ]
}

Soft-deleted states are excluded. A never-priced state appears in states[] with its identity only and no cost fields, which is different from a $0 state.

Attribution by Tag

This is the "who owns this spend" view. First discover which tag keys exist across the fleet:

stategraph cost tag-keys --tenant $TENANT_ID
# or, against the API directly:
curl -H "Authorization: Bearer $STATEGRAPH_API_KEY" \
  "http://localhost:8080/api/v1/tenants/$TENANT_ID/costs/tag-keys"
{ "tag_keys": ["Environment", "Project", "Role", "Service", "Team", "Tier"] }

Then break the rollup down by one of them with ?tag_key=:

stategraph cost tenant --tenant $TENANT_ID --tag-key Team
# or, against the API directly:
curl -H "Authorization: Bearer $STATEGRAPH_API_KEY" \
  "http://localhost:8080/api/v1/tenants/$TENANT_ID/costs?tag_key=Team"

by_tag comes back with one row per value of that key:

{
  "by_tag": [
    { "name": "data",        "monthly_cost": "1149.020000", "resource_count": 13 },
    { "name": "engineering", "monthly_cost": "7.592000",    "resource_count": 20 },
    { "name": "platform",    "monthly_cost": "2.200000",    "resource_count": 52 },
    { "name": "sre",                                         "resource_count": 17 },
    { "name": "untagged",    "monthly_cost": "415.152000",  "resource_count": 103 }
  ]
}

Two things to read carefully:

  • untagged rolls up every resource missing that key. A large untagged figure is usually a tagging-hygiene signal, not a real owner.
  • Absent money is not $0. The sre row above has 17 resources but no monthly_cost, because none of them were priced (free-to-create resources, or unsupported types). A $0 value means "priced, costs nothing"; an absent field means "nothing here was priced." Do not render them the same way.

Group by Environment, Project, Service, or any other key the same way, for example ?tag_key=Environment to split production from staging.

History is derived from the append-only snapshot trail. Each state's most recent snapshot is carried forward across recompute gaps, so the series is real, not interpolated.

stategraph cost history --tenant $TENANT_ID --group-by provider
# or, against the API directly:
curl -H "Authorization: Bearer $STATEGRAPH_API_KEY" \
  "http://localhost:8080/api/v1/tenants/$TENANT_ID/costs/history?group_by=provider"
{
  "points": [
    {
      "date": "2026-06-09",
      "monthly_cost": "1573.964000",
      "hourly_cost": "2.156116",
      "groups": [
        { "name": "aws", "monthly_cost": "1573.964000", "resource_count": 221 }
      ]
    }
  ]
}

There is one points[] entry per day, oldest first, each carrying the day's tenant total. Useful parameters:

Parameter Description
group_by provider, type, or tag, adding a groups[] breakdown to each point; with tag, set tag_key to choose the key
tag_key Tag key the per-day breakdown groups by when group_by=tag
from / to Window bounds, as full ISO 8601 / RFC 3339 timestamps (for example 2026-06-01T00:00:00Z), not bare dates; default to 30 days ago and now

A date-only value returns 400 with id INVALID_DATE_PARAM.

curl -H "Authorization: Bearer $STATEGRAPH_API_KEY" \
  "http://localhost:8080/api/v1/tenants/$TENANT_ID/costs/history?from=2026-06-01T00:00:00Z&to=2026-06-09T00:00:00Z"

Next Steps