Created
March 10, 2026 04:43
-
-
Save patmandenver/0001b0ac2500b7bfe69eaa92a8988d64 to your computer and use it in GitHub Desktop.
cost
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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