OpenRouter Team Setup Overview OpenRouter supports team usage through per-user API keys with individual credit limits, management keys for programmatic key provisioning, and usage attribution via headers. This skill covers key provisioning, per-user budgets, usage tracking, and governance policies for multi-user deployments. Key Provisioning via Management API Shared Key with User Attribution Per-User Budget Enforcement Team Usage Report Team Key Dashboard Script Model Governance Error Handling | Error | Cause | Fix | |-------|-------|-----| | Management key 403 | Using API key instead of man…

\\t'\n\necho \"\"\necho \"=== Total Usage ===\"\ncurl -s https://openrouter.ai/api/v1/keys \\\n -H \"Authorization: Bearer $OPENROUTER_MGMT_KEY\" | \\\n jq '.data | map(.usage // 0) | add | \"Total spend: $\\(.)\"'\n```\n\n## Model Governance\n\n```python\n# Define which models each tier can use\nMODEL_ALLOWLISTS = {\n \"free\": [\"google/gemma-2-9b-it:free\"],\n \"basic\": [\"openai/gpt-4o-mini\", \"meta-llama/llama-3.1-8b-instruct\"],\n \"pro\": [\"openai/gpt-4o-mini\", \"openai/gpt-4o\", \"anthropic/claude-3.5-sonnet\"],\n \"enterprise\": None, # None = all models allowed\n}\n\ndef enforce_model_policy(user_tier: str, requested_model: str) -> str:\n \"\"\"Enforce model allowlist based on user tier.\"\"\"\n allowlist = MODEL_ALLOWLISTS.get(user_tier)\n if allowlist is None:\n return requested_model # Enterprise: unrestricted\n if requested_model in allowlist:\n return requested_model\n # Downgrade to best allowed model\n return allowlist[-1]\n```\n\n## Error Handling\n\n| Error | Cause | Fix |\n|-------|-------|-----|\n| Management key 403 | Using API key instead of management key | Management keys are separate -- create one at openrouter.ai/keys |\n| User exceeds budget | No per-user limits set | Create individual keys with credit limits |\n| Attribution missing | No X-Title header | Enforce header in shared client wrapper |\n| Key sprawl | Too many keys to track | Implement key lifecycle management; revoke unused keys |\n\n## Enterprise Considerations\n\n- Use management keys for programmatic key provisioning -- they can create/list/delete API keys but cannot make completions\n- Set per-key credit limits to prevent any single user from exhausting shared budget\n- Use `X-Title` header with user identifiers for dashboard-level attribution\n- Implement model allowlists per user tier to control access to expensive models\n- Build weekly usage reports for cost visibility and anomaly detection\n- Rotate team keys on a schedule; revoke keys for departed team members immediately\n\n## References\n\n- Examples | Errors\n- [Key Provisioning](https://openrouter.ai/docs/guides/overview/auth/provisioning-api-keys) | [Auth API](https://openrouter.ai/docs/api/reference/authentication)\n---","attachment_filenames":["references/access-control.md","references/api-key-strategy-for-teams.md","references/budget-management.md","references/errors.md","references/examples.md","references/shared-services-setup.md","references/team-configuration.md","references/tracking-by-user.md"],"attachments":[{"filename":"references/access-control.md","content":"# Access Control\n\n## Access Control\n\n### Model Access by Role\n\n```python\nROLE_MODEL_ACCESS = {\n \"admin\": [\n \"anthropic/claude-3-opus\",\n \"anthropic/claude-3.5-sonnet\",\n \"anthropic/claude-3-haiku\",\n \"openai/gpt-4-turbo\",\n \"openai/gpt-4\",\n \"openai/gpt-3.5-turbo\",\n ],\n \"developer\": [\n \"anthropic/claude-3.5-sonnet\",\n \"anthropic/claude-3-haiku\",\n \"openai/gpt-4-turbo\",\n \"openai/gpt-3.5-turbo\",\n ],\n \"intern\": [\n \"anthropic/claude-3-haiku\",\n \"openai/gpt-3.5-turbo\",\n \"meta-llama/llama-3.1-8b-instruct\",\n ]\n}\n\ndef get_allowed_models(role: str) -> list:\n return ROLE_MODEL_ACCESS.get(role, ROLE_MODEL_ACCESS[\"intern\"])\n\ndef role_checked_chat(user_role: str, model: str, prompt: str):\n allowed = get_allowed_models(user_role)\n\n if model not in allowed:\n raise PermissionError(\n f\"Model {model} not available for role {user_role}. \"\n f\"Allowed: {allowed}\"\n )\n\n return client.chat.completions.create(\n model=model,\n messages=[{\"role\": \"user\", \"content\": prompt}]\n )\n```\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":1134,"content_sha256":"f583e33134605bf8ebcc57de8e49344e4cd4ffc264bc3ecd3a018a9c84f36fa8"},{"filename":"references/api-key-strategy-for-teams.md","content":"# Api Key Strategy For Teams\n\n## API Key Strategy for Teams\n\n### Separate Keys Per Environment\n\n```\nDevelopment:\n Key: sk-or-dev-xxx\n Label: \"Development\"\n Limit: $10.00\n\nStaging:\n Key: sk-or-stg-xxx\n Label: \"Staging\"\n Limit: $50.00\n\nProduction:\n Key: sk-or-prod-xxx\n Label: \"Production\"\n Limit: $500.00\n```\n\n### Keys Per Team/Service\n\n```\nFrontend Team:\n Key: sk-or-frontend-xxx\n Label: \"Frontend\"\n Limit: $100.00\n\nBackend Team:\n Key: sk-or-backend-xxx\n Label: \"Backend\"\n Limit: $200.00\n\nData Team:\n Key: sk-or-data-xxx\n Label: \"Data Processing\"\n Limit: $300.00\n```\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":585,"content_sha256":"c8f5900fdd3dae06b91e7ab379f9c698f2569d3345af63fe9eee2d35ac3322c2"},{"filename":"references/budget-management.md","content":"# Budget Management\n\n## Budget Management\n\n### Per-User Budgets\n\n```python\nclass BudgetManager:\n def __init__(self):\n self.budgets = {} # user_id -> budget\n self.spent = {} # user_id -> amount spent\n\n def set_budget(self, user_id: str, amount: float):\n self.budgets[user_id] = amount\n if user_id not in self.spent:\n self.spent[user_id] = 0.0\n\n def can_spend(self, user_id: str, amount: float) -> bool:\n budget = self.budgets.get(user_id, float('inf'))\n spent = self.spent.get(user_id, 0.0)\n return spent + amount \u003c= budget\n\n def record_spend(self, user_id: str, amount: float):\n self.spent[user_id] = self.spent.get(user_id, 0.0) + amount\n\n def get_remaining(self, user_id: str) -> float:\n budget = self.budgets.get(user_id, float('inf'))\n spent = self.spent.get(user_id, 0.0)\n return budget - spent\n\nbudget_mgr = BudgetManager()\nbudget_mgr.set_budget(\"user123\", 50.00) # $50/month\n\ndef budget_checked_chat(user_id: str, prompt: str, model: str):\n # Estimate cost\n estimated_cost = 0.01 # Rough estimate\n\n if not budget_mgr.can_spend(user_id, estimated_cost):\n raise Exception(\"Budget exceeded\")\n\n response = client.chat.completions.create(\n model=model,\n messages=[{\"role\": \"user\", \"content\": prompt}]\n )\n\n # Calculate actual cost\n actual_cost = calculate_cost(response, model)\n budget_mgr.record_spend(user_id, actual_cost)\n\n return response\n```\n\n### Team Budget Dashboard\n\n```python\nclass TeamBudgetDashboard:\n def __init__(self, team_budget: float):\n self.team_budget = team_budget\n self.user_spending = {}\n\n def record_usage(\n self,\n user_id: str,\n model: str,\n prompt_tokens: int,\n completion_tokens: int\n ):\n cost = self._calculate_cost(model, prompt_tokens, completion_tokens)\n\n if user_id not in self.user_spending:\n self.user_spending[user_id] = []\n\n self.user_spending[user_id].append({\n \"timestamp\": datetime.now(),\n \"model\": model,\n \"tokens\": prompt_tokens + completion_tokens,\n \"cost\": cost\n })\n\n def _calculate_cost(\n self,\n model: str,\n prompt_tokens: int,\n completion_tokens: int\n ) -> float:\n prices = {\n \"anthropic/claude-3.5-sonnet\": (0.003, 0.015),\n \"anthropic/claude-3-haiku\": (0.00025, 0.00125),\n \"openai/gpt-4-turbo\": (0.01, 0.03),\n }\n prompt_price, completion_price = prices.get(model, (0.01, 0.03))\n return (\n prompt_tokens * prompt_price / 1000 +\n completion_tokens * completion_price / 1000\n )\n\n def get_dashboard(self) -> dict:\n total_spent = sum(\n sum(u[\"cost\"] for u in usage)\n for usage in self.user_spending.values()\n )\n\n return {\n \"team_budget\": self.team_budget,\n \"total_spent\": total_spent,\n \"remaining\": self.team_budget - total_spent,\n \"utilization\": total_spent / self.team_budget * 100,\n \"by_user\": {\n user: sum(u[\"cost\"] for u in usage)\n for user, usage in self.user_spending.items()\n }\n }\n```\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":3306,"content_sha256":"18ea06f9457bb7f0f8f906e313759bb06ad3de56cc28674ec7934de9cb7db9ae"},{"filename":"references/errors.md","content":"# Error Handling Reference\n\nCommon errors and solutions:\n\n1. **401 Unauthorized**: Check API key format (must start with `sk-or-`)\n2. **429 Rate Limited**: Implement exponential backoff\n3. **500 Server Error**: Retry with backoff, check OpenRouter status page\n4. **Model Not Found**: Verify model ID includes provider prefix\n\n---\n*[Tons of Skills](https://tonsofskills.com) by [Intent Solutions](https://intentsolutions.io) | [jeremylongshore.com](https://jeremylongshore.com)*\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":478,"content_sha256":"7c51ab2ceb557d914330a8bcf3b68e4c14f34c0d35cb45d5ee9e693f788252ae"},{"filename":"references/examples.md","content":"# Team Setup — Runnable Examples\n\n## Python — User-Attributed API Calls\n\n```python\nimport os\nimport json\nfrom datetime import datetime\nfrom openai import OpenAI\n\nclient = OpenAI(\n base_url=\"https://openrouter.ai/api/v1\",\n api_key=os.environ[\"OPENROUTER_API_KEY\"],\n)\n\n\ndef team_complete(prompt: str, user_id: str, team: str = \"engineering\",\n model: str = \"openai/gpt-3.5-turbo\") -> dict:\n response = client.chat.completions.create(\n model=model,\n messages=[{\"role\": \"user\", \"content\": prompt}],\n max_tokens=500,\n extra_headers={\n \"X-Title\": f\"{team}/{user_id}\",\n },\n )\n return {\n \"content\": response.choices[0].message.content,\n \"user_id\": user_id,\n \"team\": team,\n \"model\": response.model,\n \"tokens\": response.usage.total_tokens,\n }\n\n\ndef tracked_complete(prompt: str, user_id: str, team: str) -> str:\n result = team_complete(prompt, user_id, team)\n log_entry = {\n \"timestamp\": datetime.utcnow().isoformat(),\n \"user_id\": user_id,\n \"team\": team,\n \"model\": result[\"model\"],\n \"tokens\": result[\"tokens\"],\n }\n with open(\"usage-audit.jsonl\", \"a\") as f:\n f.write(json.dumps(log_entry) + \"\\n\")\n return result[\"content\"]\n\n\nprint(tracked_complete(\"Summarize agile methodology\", user_id=\"alice\", team=\"product\"))\nprint(tracked_complete(\"Write a Python sort function\", user_id=\"bob\", team=\"engineering\"))\n```\n\n## Python — Per-User Budget Enforcement\n\n```python\nimport os\nimport json\nfrom pathlib import Path\nfrom openai import OpenAI\n\nclient = OpenAI(\n base_url=\"https://openrouter.ai/api/v1\",\n api_key=os.environ[\"OPENROUTER_API_KEY\"],\n)\n\nMODEL_COST_PER_M = {\n \"openai/gpt-3.5-turbo\": 1.0,\n \"anthropic/claude-3.5-sonnet\": 15.0,\n \"google/gemma-2-9b-it:free\": 0.0,\n}\n\nDAILY_BUDGET_USD = {\"free_tier\": 0.10, \"standard\": 1.00, \"premium\": 10.00}\n\n\nclass UserBudgetTracker:\n def __init__(self, db_path: str = \"budgets.json\"):\n self.db_path = Path(db_path)\n self._data: dict = json.loads(self.db_path.read_text()) if self.db_path.exists() else {}\n\n def _user_key(self, user_id: str) -> str:\n from datetime import date\n return f\"{user_id}:{date.today().isoformat()}\"\n\n def get_spent(self, user_id: str) -> float:\n return self._data.get(self._user_key(user_id), 0.0)\n\n def add_spend(self, user_id: str, tokens: int, model: str) -> float:\n cost = tokens * MODEL_COST_PER_M.get(model, 1.0) / 1_000_000\n key = self._user_key(user_id)\n self._data[key] = self._data.get(key, 0.0) + cost\n self.db_path.write_text(json.dumps(self._data, indent=2))\n return cost\n\n def check_budget(self, user_id: str, tier: str = \"standard\") -> bool:\n return self.get_spent(user_id) \u003c DAILY_BUDGET_USD.get(tier, 1.00)\n\n\ntracker = UserBudgetTracker()\n\ndef budget_complete(prompt: str, user_id: str, tier: str = \"standard\") -> str:\n if not tracker.check_budget(user_id, tier):\n raise RuntimeError(f\"Daily budget exceeded for {user_id}\")\n\n model = \"google/gemma-2-9b-it:free\" if tier == \"free_tier\" else \"openai/gpt-3.5-turbo\"\n response = client.chat.completions.create(\n model=model, messages=[{\"role\": \"user\", \"content\": prompt}], max_tokens=300,\n )\n cost = tracker.add_spend(user_id, response.usage.total_tokens, model)\n print(f\"[Budget] {user_id}: +${cost:.4f} (total ${tracker.get_spent(user_id):.4f} today)\")\n return response.choices[0].message.content\n\n\nprint(budget_complete(\"What is Python?\", \"alice\", \"standard\"))\n```\n\n---\n*[Tons of Skills](https://tonsofskills.com) by [Intent Solutions](https://intentsolutions.io) | [jeremylongshore.com](https://jeremylongshore.com)*\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":3753,"content_sha256":"ec1c0ae23cf5da36776a872f355b0b75c8fb151cd7422c44e6f06160ac987cc6"},{"filename":"references/shared-services-setup.md","content":"# Shared Services Setup\n\n## Shared Services Setup\n\n### Central LLM Service\n\n```python\nfrom fastapi import FastAPI, HTTPException, Depends\nfrom fastapi.security import HTTPBearer, HTTPAuthorizationCredentials\n\napp = FastAPI()\nsecurity = HTTPBearer()\n\n# User database (in practice, use real auth)\nUSERS = {\n \"token123\": {\"user_id\": \"alice\", \"role\": \"admin\"},\n \"token456\": {\"user_id\": \"bob\", \"role\": \"developer\"},\n}\n\nasync def get_current_user(\n credentials: HTTPAuthorizationCredentials = Depends(security)\n):\n token = credentials.credentials\n if token not in USERS:\n raise HTTPException(status_code=401, detail=\"Invalid token\")\n return USERS[token]\n\[email protected](\"/chat\")\nasync def chat(\n prompt: str,\n model: str = \"anthropic/claude-3.5-sonnet\",\n user: dict = Depends(get_current_user)\n):\n # Check model access\n allowed = get_allowed_models(user[\"role\"])\n if model not in allowed:\n raise HTTPException(\n status_code=403,\n detail=f\"Model {model} not allowed for your role\"\n )\n\n # Check budget\n if not budget_mgr.can_spend(user[\"user_id\"], 0.01):\n raise HTTPException(status_code=402, detail=\"Budget exceeded\")\n\n # Make request\n response = client.chat.completions.create(\n model=model,\n messages=[{\"role\": \"user\", \"content\": prompt}]\n )\n\n # Track usage\n tracker.record(user[\"user_id\"], model, response)\n\n return {\"response\": response.choices[0].message.content}\n```\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":1485,"content_sha256":"d63419aa9d6e7f237aaa6bf65a3fb7fb67db17c9c3c8020fe218999d8a048034"},{"filename":"references/team-configuration.md","content":"# Team Configuration\n\n## Team Configuration\n\n### Shared Configuration\n\n```python\n# team_config.py\nfrom dataclasses import dataclass\nfrom typing import Optional, List\nimport os\n\n@dataclass\nclass TeamMember:\n name: str\n email: str\n role: str # \"admin\", \"developer\", \"viewer\"\n api_key_access: List[str] # Which keys they can use\n\n@dataclass\nclass TeamConfig:\n name: str\n default_model: str\n allowed_models: List[str]\n budget_limit: float\n members: List[TeamMember]\n\n# Example configuration\nTEAM_CONFIG = TeamConfig(\n name=\"Engineering Team\",\n default_model=\"anthropic/claude-3.5-sonnet\",\n allowed_models=[\n \"anthropic/claude-3.5-sonnet\",\n \"anthropic/claude-3-haiku\",\n \"openai/gpt-4-turbo\",\n \"openai/gpt-3.5-turbo\",\n \"meta-llama/llama-3.1-70b-instruct\",\n ],\n budget_limit=1000.00,\n members=[\n TeamMember(\"Alice\", \"[email protected]\", \"admin\", [\"prod\", \"dev\"]),\n TeamMember(\"Bob\", \"[email protected]\", \"developer\", [\"dev\"]),\n TeamMember(\"Carol\", \"[email protected]\", \"developer\", [\"dev\"]),\n ]\n)\n```\n\n### Role-Based Access\n\n```python\nclass TeamOpenRouter:\n def __init__(self, team_config: TeamConfig):\n self.config = team_config\n self.keys = {\n \"prod\": os.environ.get(\"OPENROUTER_PROD_KEY\"),\n \"dev\": os.environ.get(\"OPENROUTER_DEV_KEY\"),\n }\n\n def get_client_for_user(self, email: str, environment: str = \"dev\"):\n # Find user\n member = next(\n (m for m in self.config.members if m.email == email),\n None\n )\n if not member:\n raise PermissionError(f\"User {email} not in team\")\n\n # Check access\n if environment not in member.api_key_access:\n raise PermissionError(\n f\"User {email} doesn't have access to {environment}\"\n )\n\n # Return client\n return OpenAI(\n base_url=\"https://openrouter.ai/api/v1\",\n api_key=self.keys[environment],\n default_headers={\"X-Title\": f\"{self.config.name} - {email}\"}\n )\n\n def chat(self, email: str, prompt: str, model: str = None, **kwargs):\n # Validate model\n model = model or self.config.default_model\n if model not in self.config.allowed_models:\n raise ValueError(\n f\"Model {model} not allowed. Use: {self.config.allowed_models}\"\n )\n\n client = self.get_client_for_user(email)\n return client.chat.completions.create(\n model=model,\n messages=[{\"role\": \"user\", \"content\": prompt}],\n **kwargs\n )\n\nteam_router = TeamOpenRouter(TEAM_CONFIG)\n```\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":2691,"content_sha256":"bb7520b2fd16a4907824829381f2eea3c4423891b29c4be8a6709acae31686e7"},{"filename":"references/tracking-by-user.md","content":"# Tracking By User\n\n## Tracking by User\n\n### User Attribution\n\n```python\nclass TrackedTeamClient:\n def __init__(self, api_key: str):\n self.client = OpenAI(\n base_url=\"https://openrouter.ai/api/v1\",\n api_key=api_key\n )\n self.usage_log = []\n\n def chat(\n self,\n prompt: str,\n user_id: str,\n model: str = \"anthropic/claude-3.5-sonnet\",\n **kwargs\n ):\n start_time = time.time()\n\n response = self.client.chat.completions.create(\n model=model,\n messages=[{\"role\": \"user\", \"content\": prompt}],\n **kwargs\n )\n\n # Log usage\n self.usage_log.append({\n \"timestamp\": datetime.now().isoformat(),\n \"user_id\": user_id,\n \"model\": model,\n \"prompt_tokens\": response.usage.prompt_tokens,\n \"completion_tokens\": response.usage.completion_tokens,\n \"latency_ms\": (time.time() - start_time) * 1000\n })\n\n return response\n\n def get_user_usage(self, user_id: str) -> dict:\n user_logs = [l for l in self.usage_log if l[\"user_id\"] == user_id]\n return {\n \"user_id\": user_id,\n \"total_requests\": len(user_logs),\n \"total_tokens\": sum(\n l[\"prompt_tokens\"] + l[\"completion_tokens\"]\n for l in user_logs\n )\n }\n\n def get_team_summary(self) -> dict:\n by_user = {}\n for log in self.usage_log:\n uid = log[\"user_id\"]\n if uid not in by_user:\n by_user[uid] = {\"requests\": 0, \"tokens\": 0}\n by_user[uid][\"requests\"] += 1\n by_user[uid][\"tokens\"] += (\n log[\"prompt_tokens\"] + log[\"completion_tokens\"]\n )\n return by_user\n```\n\n### HTTP Headers for Tracking\n\n```python\ndef create_team_client(user_email: str, team_name: str):\n \"\"\"Create client with tracking headers.\"\"\"\n return OpenAI(\n base_url=\"https://openrouter.ai/api/v1\",\n api_key=os.environ[\"OPENROUTER_API_KEY\"],\n default_headers={\n \"HTTP-Referer\": f\"https://{team_name}.company.com\",\n \"X-Title\": f\"{team_name} - {user_email}\",\n }\n )\n```\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":2242,"content_sha256":"649f03c8e3b40605e17fa9eeaf48e13bc77d9c3f76a452b37d4f04b9d87ddd66"}],"content_json":{"type":"doc","content":[{"type":"heading","attrs":{"level":1},"content":[{"text":"OpenRouter Team Setup","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Overview","type":"text"}]},{"type":"paragraph","content":[{"text":"OpenRouter supports team usage through per-user API keys with individual credit limits, management keys for programmatic key provisioning, and usage attribution via headers. This skill covers key provisioning, per-user budgets, usage tracking, and governance policies for multi-user deployments.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Key Provisioning via Management API","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"python"},"content":[{"text":"import os, requests\n\nMGMT_KEY = os.environ[\"OPENROUTER_MGMT_KEY\"] # Management key (cannot call completions)\n\ndef create_team_key(name: str, credit_limit: float = 25.0) -> dict:\n \"\"\"Create a new API key for a team member.\"\"\"\n resp = requests.post(\n \"https://openrouter.ai/api/v1/keys\",\n headers={\"Authorization\": f\"Bearer {MGMT_KEY}\"},\n json={\"name\": name, \"limit\": credit_limit},\n )\n resp.raise_for_status()\n data = resp.json()[\"data\"]\n return {\n \"key\": data[\"key\"], # sk-or-v1-... (shown once)\n \"hash\": data[\"key_hash\"], # For later identification\n \"name\": name,\n \"limit\": credit_limit,\n }\n\ndef list_team_keys() -> list[dict]:\n \"\"\"List all keys with usage and limits.\"\"\"\n resp = requests.get(\n \"https://openrouter.ai/api/v1/keys\",\n headers={\"Authorization\": f\"Bearer {MGMT_KEY}\"},\n )\n return [\n {\n \"name\": k.get(\"name\"),\n \"hash\": k.get(\"key_hash\"),\n \"usage\": k.get(\"usage\", 0),\n \"limit\": k.get(\"limit\"),\n \"is_free_tier\": k.get(\"is_free_tier\", False),\n }\n for k in resp.json().get(\"data\", [])\n ]\n\ndef delete_team_key(key_hash: str):\n \"\"\"Revoke a team member's key.\"\"\"\n resp = requests.delete(\n f\"https://openrouter.ai/api/v1/keys/{key_hash}\",\n headers={\"Authorization\": f\"Bearer {MGMT_KEY}\"},\n )\n resp.raise_for_status()\n\n# Provision keys for the team\nfor member in [\"alice-backend\", \"bob-frontend\", \"carol-ml\"]:\n key_info = create_team_key(member, credit_limit=50.0)\n print(f\"Created key for {member}: {key_info['key'][:20]}...\")","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Shared Key with User Attribution","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"python"},"content":[{"text":"from openai import OpenAI\n\n# Alternative: single shared key with user identification via headers\ndef get_client_for_user(user_id: str) -> OpenAI:\n \"\"\"Create a client that attributes usage to a specific user.\"\"\"\n return OpenAI(\n base_url=\"https://openrouter.ai/api/v1\",\n api_key=os.environ[\"OPENROUTER_API_KEY\"],\n default_headers={\n \"HTTP-Referer\": \"https://my-app.com\",\n \"X-Title\": f\"my-app:{user_id}\", # User shows in dashboard\n },\n )\n\n# Each user's requests appear under their X-Title in the dashboard\nalice_client = get_client_for_user(\"alice\")\nresponse = alice_client.chat.completions.create(\n model=\"openai/gpt-4o-mini\",\n messages=[{\"role\": \"user\", \"content\": \"Hello\"}],\n max_tokens=100,\n)","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Per-User Budget Enforcement","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"python"},"content":[{"text":"import sqlite3, time\n\ndef init_team_db(db_path: str = \"team_usage.db\"):\n conn = sqlite3.connect(db_path)\n conn.execute(\"\"\"\n CREATE TABLE IF NOT EXISTS user_usage (\n user_id TEXT NOT NULL,\n date TEXT NOT NULL,\n total_cost REAL DEFAULT 0,\n request_count INTEGER DEFAULT 0,\n PRIMARY KEY (user_id, date)\n )\n \"\"\")\n conn.execute(\"\"\"\n CREATE TABLE IF NOT EXISTS user_budgets (\n user_id TEXT PRIMARY KEY,\n daily_limit REAL NOT NULL,\n model_allowlist TEXT -- JSON array of allowed model IDs\n )\n \"\"\")\n conn.commit()\n return conn\n\ndef check_user_budget(conn, user_id: str) -> bool:\n \"\"\"Check if user is within their daily budget.\"\"\"\n today = time.strftime(\"%Y-%m-%d\")\n row = conn.execute(\n \"SELECT u.total_cost, b.daily_limit FROM user_usage u \"\n \"JOIN user_budgets b ON u.user_id = b.user_id \"\n \"WHERE u.user_id = ? AND u.date = ?\",\n (user_id, today),\n ).fetchone()\n\n if not row:\n return True # No usage yet today\n return row[0] \u003c row[1]\n\ndef record_user_usage(conn, user_id: str, cost: float):\n \"\"\"Record a request's cost for a user.\"\"\"\n today = time.strftime(\"%Y-%m-%d\")\n conn.execute(\n \"\"\"INSERT INTO user_usage (user_id, date, total_cost, request_count)\n VALUES (?, ?, ?, 1)\n ON CONFLICT(user_id, date) DO UPDATE SET\n total_cost = total_cost + ?, request_count = request_count + 1\"\"\",\n (user_id, today, cost, cost),\n )\n conn.commit()","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Team Usage Report","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"python"},"content":[{"text":"def team_usage_report(conn) -> list[dict]:\n \"\"\"Generate a team usage report for the current week.\"\"\"\n rows = conn.execute(\"\"\"\n SELECT u.user_id, SUM(u.total_cost) as weekly_cost,\n SUM(u.request_count) as requests,\n b.daily_limit\n FROM user_usage u\n JOIN user_budgets b ON u.user_id = b.user_id\n WHERE u.date >= date('now', '-7 days')\n GROUP BY u.user_id\n ORDER BY weekly_cost DESC\n \"\"\").fetchall()\n\n return [\n {\n \"user\": row[0],\n \"weekly_cost\": round(row[1], 4),\n \"requests\": row[2],\n \"daily_limit\": row[3],\n }\n for row in rows\n ]","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Team Key Dashboard Script","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"#!/bin/bash\n# Show all team keys with usage\n\necho \"=== OpenRouter Team Keys ===\"\ncurl -s https://openrouter.ai/api/v1/keys \\\n -H \"Authorization: Bearer $OPENROUTER_MGMT_KEY\" | \\\n jq -r '.data[] | \"\\(.name)\\t$\\(.usage // 0 | tostring)\\t/\\t$\\(.limit // \"unlimited\" | tostring)\"' | \\\n column -t -s

OpenRouter Team Setup Overview OpenRouter supports team usage through per-user API keys with individual credit limits, management keys for programmatic key provisioning, and usage attribution via headers. This skill covers key provisioning, per-user budgets, usage tracking, and governance policies for multi-user deployments. Key Provisioning via Management API Shared Key with User Attribution Per-User Budget Enforcement Team Usage Report Team Key Dashboard Script Model Governance Error Handling | Error | Cause | Fix | |-------|-------|-----| | Management key 403 | Using API key instead of man…

\\t'\n\necho \"\"\necho \"=== Total Usage ===\"\ncurl -s https://openrouter.ai/api/v1/keys \\\n -H \"Authorization: Bearer $OPENROUTER_MGMT_KEY\" | \\\n jq '.data | map(.usage // 0) | add | \"Total spend: $\\(.)\"'","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Model Governance","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"python"},"content":[{"text":"# Define which models each tier can use\nMODEL_ALLOWLISTS = {\n \"free\": [\"google/gemma-2-9b-it:free\"],\n \"basic\": [\"openai/gpt-4o-mini\", \"meta-llama/llama-3.1-8b-instruct\"],\n \"pro\": [\"openai/gpt-4o-mini\", \"openai/gpt-4o\", \"anthropic/claude-3.5-sonnet\"],\n \"enterprise\": None, # None = all models allowed\n}\n\ndef enforce_model_policy(user_tier: str, requested_model: str) -> str:\n \"\"\"Enforce model allowlist based on user tier.\"\"\"\n allowlist = MODEL_ALLOWLISTS.get(user_tier)\n if allowlist is None:\n return requested_model # Enterprise: unrestricted\n if requested_model in allowlist:\n return requested_model\n # Downgrade to best allowed model\n return allowlist[-1]","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Error Handling","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Error","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Cause","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Fix","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Management key 403","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Using API key instead of management key","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Management keys are separate -- create one at openrouter.ai/keys","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"User exceeds budget","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"No per-user limits set","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Create individual keys with credit limits","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Attribution missing","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"No X-Title header","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Enforce header in shared client wrapper","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Key sprawl","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Too many keys to track","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Implement key lifecycle management; revoke unused keys","type":"text"}]}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Enterprise Considerations","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Use management keys for programmatic key provisioning -- they can create/list/delete API keys but cannot make completions","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Set per-key credit limits to prevent any single user from exhausting shared budget","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Use ","type":"text"},{"text":"X-Title","type":"text","marks":[{"type":"code_inline"}]},{"text":" header with user identifiers for dashboard-level attribution","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Implement model allowlists per user tier to control access to expensive models","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Build weekly usage reports for cost visibility and anomaly detection","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Rotate team keys on a schedule; revoke keys for departed team members immediately","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"References","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Examples | Errors","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Key Provisioning","type":"text","marks":[{"type":"link","attrs":{"href":"https://openrouter.ai/docs/guides/overview/auth/provisioning-api-keys","title":null}}]},{"text":" | ","type":"text"},{"text":"Auth API","type":"text","marks":[{"type":"link","attrs":{"href":"https://openrouter.ai/docs/api/reference/authentication","title":null}}]}]}]}]},{"type":"hr","attrs":{"markup":"---"}}]},"metadata":{"date":"2026-06-05","name":"openrouter-team-setup","tags":["saas","openrouter","team","organization","governance"],"author":"@skillopedia","source":{"stars":2275,"repo_name":"claude-code-plugins-plus-skills","origin_url":"https://github.com/jeremylongshore/claude-code-plugins-plus-skills/blob/HEAD/plugins/saas-packs/openrouter-pack/skills/openrouter-team-setup/SKILL.md","repo_owner":"jeremylongshore","body_sha256":"961267da58a9ad82fc8cde67525e079c8318f203b23f34db759f321b560f2ba9","cluster_key":"eca46923cf98156f1b893982f94cc0e7da8dd788e44c725760527e610762781c","clean_bundle":{"format":"clean-skill-bundle-v1","source":"jeremylongshore/claude-code-plugins-plus-skills/plugins/saas-packs/openrouter-pack/skills/openrouter-team-setup/SKILL.md","attachments":[{"id":"8a4f100e-7c87-5af2-8438-e12b9aa77fad","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/8a4f100e-7c87-5af2-8438-e12b9aa77fad/attachment.md","path":"references/access-control.md","size":1134,"sha256":"f583e33134605bf8ebcc57de8e49344e4cd4ffc264bc3ecd3a018a9c84f36fa8","contentType":"text/markdown; charset=utf-8"},{"id":"0e3326cb-dd9d-5395-b378-29f5d361cd52","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/0e3326cb-dd9d-5395-b378-29f5d361cd52/attachment.md","path":"references/api-key-strategy-for-teams.md","size":585,"sha256":"c8f5900fdd3dae06b91e7ab379f9c698f2569d3345af63fe9eee2d35ac3322c2","contentType":"text/markdown; charset=utf-8"},{"id":"bb81a5c2-ae76-58e1-9c4b-32f1d17fbe5f","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/bb81a5c2-ae76-58e1-9c4b-32f1d17fbe5f/attachment.md","path":"references/budget-management.md","size":3306,"sha256":"18ea06f9457bb7f0f8f906e313759bb06ad3de56cc28674ec7934de9cb7db9ae","contentType":"text/markdown; charset=utf-8"},{"id":"6c6132d1-bf25-53a6-bc14-43d11bd28650","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/6c6132d1-bf25-53a6-bc14-43d11bd28650/attachment.md","path":"references/errors.md","size":478,"sha256":"7c51ab2ceb557d914330a8bcf3b68e4c14f34c0d35cb45d5ee9e693f788252ae","contentType":"text/markdown; charset=utf-8"},{"id":"95d14b58-f019-5375-ba5f-08bdb9a14854","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/95d14b58-f019-5375-ba5f-08bdb9a14854/attachment.md","path":"references/examples.md","size":3753,"sha256":"ec1c0ae23cf5da36776a872f355b0b75c8fb151cd7422c44e6f06160ac987cc6","contentType":"text/markdown; charset=utf-8"},{"id":"82a90072-3810-59fb-8a5d-0cb0b3dab572","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/82a90072-3810-59fb-8a5d-0cb0b3dab572/attachment.md","path":"references/shared-services-setup.md","size":1485,"sha256":"d63419aa9d6e7f237aaa6bf65a3fb7fb67db17c9c3c8020fe218999d8a048034","contentType":"text/markdown; charset=utf-8"},{"id":"535878ae-81ed-522d-848a-5b3f513a6c25","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/535878ae-81ed-522d-848a-5b3f513a6c25/attachment.md","path":"references/team-configuration.md","size":2691,"sha256":"bb7520b2fd16a4907824829381f2eea3c4423891b29c4be8a6709acae31686e7","contentType":"text/markdown; charset=utf-8"},{"id":"9efc7f40-3478-5031-8d39-e4088ba193b3","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/9efc7f40-3478-5031-8d39-e4088ba193b3/attachment.md","path":"references/tracking-by-user.md","size":2242,"sha256":"649f03c8e3b40605e17fa9eeaf48e13bc77d9c3f76a452b37d4f04b9d87ddd66","contentType":"text/markdown; charset=utf-8"}],"bundle_sha256":"1cecc1103d94552772ddcd292891877d8fda40f26c224fd18a91f3818a868fc0","attachment_count":8,"text_attachments":8,"attachment_storage":"skillopedia-attachments-v1","binary_attachments":0,"excluded_attachments":[]},"cluster_size":1,"skill_md_path":"plugins/saas-packs/openrouter-pack/skills/openrouter-team-setup/SKILL.md","import_metadata":{"date":"2026-06-05","author":"@skillopedia","version":"v1","category":"integrations-apis","category_label":"Integrations"},"exact_dupes_collapsed_into_this":0},"license":"MIT","version":"v1","category":"integrations-apis","import_tag":"clean-skills-v1","description":"Configure OpenRouter for multi-user teams with per-user keys, budget controls, and usage attribution. Triggers: 'openrouter team', 'openrouter multi-user', 'openrouter organization', 'team api keys openrouter'.\n","allowed-tools":"Read, Write, Edit, Bash, Grep","compatibility":"Designed for Claude Code, also compatible with Codex and OpenClaw"}},"renderedAt":1782980742777}

OpenRouter Team Setup Overview OpenRouter supports team usage through per-user API keys with individual credit limits, management keys for programmatic key provisioning, and usage attribution via headers. This skill covers key provisioning, per-user budgets, usage tracking, and governance policies for multi-user deployments. Key Provisioning via Management API Shared Key with User Attribution Per-User Budget Enforcement Team Usage Report Team Key Dashboard Script Model Governance Error Handling | Error | Cause | Fix | |-------|-------|-----| | Management key 403 | Using API key instead of man…