Skip to content

Instantly share code, notes, and snippets.

@patmandenver
Created March 10, 2026 04:43
Show Gist options
  • Select an option

  • Save patmandenver/0001b0ac2500b7bfe69eaa92a8988d64 to your computer and use it in GitHub Desktop.

Select an option

Save patmandenver/0001b0ac2500b7bfe69eaa92a8988d64 to your computer and use it in GitHub Desktop.
cost
#!/usr/bin/env python3
"""
Author: T. Patrick Bailey
Whiteboarcoder.com
Returns the monthly cost/budget for Claude
Assumes an admin key is at ~/.claude-key-cost
and you have to hard code the BUDGET variable
"""
import os
import sys
from datetime import datetime, timezone
from dateutil.relativedelta import relativedelta
import requests
#AS of 3/9/26 you can't pull Budget limit from the API :(
#hardcodingit :(
BUDGET=20.00
# ------------------------------------------------
# Colors
# ------------------------------------------------
RED = "\033[91m"
GREEN = "\033[92m"
YELLOW = "\033[93m"
RESET = "\033[0m"
def color_text(text: str, color: str) -> str:
return f"{color}{text}{RESET}"
def color_days(days_left: int) -> str:
text = f"Budget resets in {days_left} days"
if days_left <= 5:
return color_text(text, RED)
if days_left <= 10:
return color_text(text, YELLOW)
return text
# ------------------------------------------------
# Load admin key
# ------------------------------------------------
def get_admin_key() -> str:
key_path = os.path.expanduser("~/.claude-key-cost")
if os.path.isfile(key_path) and os.access(key_path, os.R_OK):
with open(key_path, "r") as f:
content = f.read().strip()
if content.startswith("sk-ant-admin"):
return content
print("Error: Admin key not found in env or ~/.claude-key-cost", file=sys.stderr)
sys.exit(1)
# ------------------------------------------------
# Fetch workspace ID → name map (paginated too, but usually small)
# ------------------------------------------------
def fetch_workspace_map(api_key: str) -> dict:
headers = {
"x-api-key": api_key,
"anthropic-version": "2023-06-01",
"Accept": "application/json",
}
url = "https://api.anthropic.com/v1/organizations/workspaces"
params = {"limit": 100, "include_archived": "false"}
mapping = {}
while True:
try:
r = requests.get(url, headers=headers, params=params, timeout=15)
r.raise_for_status()
page = r.json()
except requests.RequestException as e:
print(f"Workspaces fetch failed: {e}", file=sys.stderr)
return mapping
for ws in page.get("data", []):
ws_id = ws.get("id")
name = ws.get("name") or ws.get("workspace_name")
if ws_id and name:
mapping[ws_id] = name
if not page.get("has_more"):
break
params["after"] = page.get("next_page") # or "page" depending on exact key
return mapping
# ------------------------------------------------
# Fetch ALL cost report data with pagination
# ------------------------------------------------
def fetch_all_costs(api_key: str, start_iso: str, end_iso: str) -> list:
headers = {
"x-api-key": api_key,
"anthropic-version": "2023-06-01",
"Accept": "application/json",
}
url = "https://api.anthropic.com/v1/organizations/cost_report"
params = {
"starting_at": start_iso,
"ending_at": end_iso,
"group_by[]": "workspace_id",
# Optional: "limit": 100, # if supported; test with/without
}
all_results = []
page_count = 0
max_pages = 50 # safety net
while page_count < max_pages:
try:
r = requests.get(url, headers=headers, params=params, timeout=30)
r.raise_for_status()
data = r.json()
except requests.RequestException as e:
print(f"Cost report page {page_count+1} failed: {e}", file=sys.stderr)
if 'r' in locals():
print(r.text, file=sys.stderr)
break
for d in data.get("data", []):
results = d.get("results", [])
#This will skip empty results (days in which nothing happens
if results:
all_results.extend(results)
if not data.get("has_more"):
break
next_token = data.get("next_page")
if not next_token:
break
params["page"] = next_token # docs use "page=..." for next requests
page_count += 1
if page_count >= max_pages:
print("Warning: Hit max pages safety limit ΓÇö data may be incomplete", file=sys.stderr)
return all_results
# ------------------------------------------------
# Main
# ------------------------------------------------
def main():
api_key = get_admin_key()
ws_map = fetch_workspace_map(api_key)
now = datetime.now(timezone.utc)
start_month = now.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
next_month = start_month + relativedelta(months=1)
end_month = next_month - relativedelta(microseconds=1)
days_in_month = (end_month - start_month).days + 1
days_left = days_in_month - (now - start_month).days
all_results = fetch_all_costs(
api_key,
start_month.isoformat(),
now.isoformat() # up to now, not end of month
)
spent_arr = {}
for result in all_results:
ws_id = result.get("workspace_id")
if not ws_id:
continue
name = ws_map.get(ws_id, ws_id[:12] + "…" if len(ws_id) > 12 else ws_id)
cost = float(result.get("amount", 0))
spent_arr[name] = spent_arr.get(name, 0.0) + cost
# Output
print(f"\nClaude Monthly Usage Report ΓÇö {start_month.strftime('%Y-%m')}")
print(f" As of: {now.strftime('%Y-%m-%d %H:%M UTC')}")
print(f" Days left: {days_left}")
print("ΓöÇ" * 60)
if not spent_arr:
print("No data or budgets configured.")
return
for workspace in spent_arr:
spent=spent_arr[workspace]/100
remain = BUDGET - spent
pct = (spent / BUDGET * 100) if BUDGET > 0 else 0
#Under 15% left
remain_color = GREEN if pct <= 85 else RED
print(f"Workspace: {workspace}")
print(f" Budget: ${BUDGET:8,.2f}")
print(f" Spent: ${spent:8,.2f} ({pct:5.1f}%)")
print(f" Remaining: {color_text(f'${remain:8,.2f}', remain_color)}")
print(f" {color_days(days_left)}")
print()
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment