Module 2: OWASP LLM Top 10 Deep Dive
7. LLM09 and LLM10: Misinformation and Unbounded Consumption
Description
In the previous lessons you analyzed vulnerabilities that exploit inputs (LLM01-LLM04), outputs (LLM05), permissions (LLM06), prompts (LLM07), and embeddings (LLM08). This lesson closes the loop with the two remaining vulnerabilities in the OWASP LLM Top 10: LLM09 (Misinformation) and LLM10 (Unbounded Consumption). Although they are often seen as the least glamorous of the Top 10, both can cause significant damage — one destroys trust, the other destroys budgets.
LLM09 (Misinformation) treats the model's hallucinations not as a product quality problem, but as a security risk. When a medical chatbot invents a drug's side effects, it is not simply a "wrong answer" — it is a vector that can cause real physical harm. When a legal assistant fabricates case law that does not exist, it is not a bug — it is a liability that can cost millions.
LLM10 (Unbounded Consumption) addresses resource exhaustion: token exhaustion, API key abuse, and denial-of-wallet attacks. Unlike a traditional DDoS that tries to bring down a server, a consumption attack against an LLM system aims to generate massive bills. An attacker with your API key can generate $50,000 in costs in a single night. An automated script can send prompts designed to maximize tokens and drain your monthly budget in hours.
Both vulnerabilities share something: their defenses are more operational than technical. You don't solve them with a single filter — you mitigate them with verification pipelines, rate limiting, cost monitoring, and continuous processes.
LLM09: Misinformation
What is it?
Misinformation in the LLM context is the generation of false, fabricated, or misleading information that the model presents with high confidence. The technical term is "hallucination", but calling it a hallucination minimizes the impact. When the model invents data, citations, or facts and presents them as truth, the user has no way to distinguish them from real information — especially in domains where they are not an expert.
Why is it a security risk (and not just a quality problem)?
The difference between a "quality bug" and a "security risk" depends on context:
| Context | Misinformation | Classification |
|---|---|---|
| Recipe chatbot | "Cook the pasta for 45 minutes" | Quality bug — overcooked pasta |
| Medical assistant | "Take 500mg of [drug] every 4 hours" | Security risk — potential overdose |
| Legal assistant | "According to the ruling García vs. State (2023)..." | Legal risk — fabricated citation in an official document |
| Financial chatbot | "This fund has a guaranteed 12% return" | Regulatory risk — false claims |
| Technical support | "To reset your router, press the red button for 10 seconds" | Minor bug — if there is no red button |
| Compliance assistant | "Under GDPR Art. 47, you can store PII without consent" | Critical risk — regulatory violation |
The right question is not "does the model hallucinate?" (it always will to some degree) but "what happens when it hallucinates in MY context?"
Types of misinformation generated by LLMs
Fabrication of data and statistics
# The model generates statistics that sound convincing but are made up
user_question = "What is the success rate of surgery X?"
# Model response (fabricated):
llm_response = """
Surgery X has a 94.7% success rate according to a study
published in The Lancet in 2024 by Dr. James Robertson et al.
The main risks include infection (2.3%) and bleeding (1.8%).
"""
# Problems:
# 1. The study does not exist
# 2. Dr. James Robertson did not publish that paper
# 3. The statistics (94.7%, 2.3%, 1.8%) are fabricated
# 4. EVERYTHING sounds extremely credible and specific
Fabrication of citations and references
# The model invents complete bibliographic references
user_question = "Give me 3 sources on AI security"
# Model response (partially fabricated):
llm_response = """
1. Smith, J. & Patel, R. (2024). "AI Security in Enterprise Systems:
A Comprehensive Framework." IEEE Security & Privacy, 22(3), 45-62.
2. Chen, L. (2023). "Adversarial Attacks on Large Language Models:
Taxonomy and Defenses." ACM Computing Surveys, 56(1), 1-35.
3. Williams, K. et al. (2025). "OWASP for AI: Bridging Traditional
and AI Security." Journal of Cybersecurity, 11(2), 112-128.
"""
# The format is perfect: authors, year, title, journal, volume, pages
# But some or all of them may be fabricated
# A non-expert user cannot verify without searching for each reference
Extreme confidence in incorrect answers
# The dangerous part is not that the model is wrong —
# it is that it is wrong with the same confidence as when it is right
correct_response = "Python 3.10 introduced pattern matching with match/case."
# Apparent confidence: high — AND IT IS CORRECT
fabricated_response = "Python 3.10 introduced the pipeline operator |> to chain functions."
# Apparent confidence: high — BUT IT IS FALSE
# The |> operator does not exist in Python
# The user cannot distinguish between the two answers
# based on the tone or structure of the response
Mitigation: Fact-checking pipeline
from pydantic import BaseModel, Field
from enum import Enum
from datetime import datetime
class VerificationStatus(str, Enum):
VERIFIED = "verified"
UNVERIFIED = "unverified"
CONTRADICTED = "contradicted"
PARTIALLY_VERIFIED = "partially_verified"
class FactCheck(BaseModel):
claim: str
status: VerificationStatus
source: str | None = None
confidence: float = Field(ge=0.0, le=1.0)
notes: str = ""
class VerifiedResponse(BaseModel):
"""LLM response with verification metadata."""
original_response: str
fact_checks: list[FactCheck] = Field(default_factory=list)
overall_confidence: float = Field(ge=0.0, le=1.0, default=0.5)
disclaimer: str = ""
sources_cited: list[str] = Field(default_factory=list)
generated_at: datetime = Field(default_factory=datetime.now)
@property
def is_safe_to_display(self) -> bool:
if not self.fact_checks:
return False
contradicted = [fc for fc in self.fact_checks if fc.status == VerificationStatus.CONTRADICTED]
return len(contradicted) == 0 and self.overall_confidence >= 0.7
class FactCheckPipeline:
"""Fact verification pipeline for LLM responses."""
def __init__(self, known_facts: dict[str, str] | None = None):
self.known_facts = known_facts or {}
def check_response(
self, llm_response: str, domain: str = "general",
) -> VerifiedResponse:
"""Verify an LLM response against known sources."""
fact_checks: list[FactCheck] = []
claims = self._extract_claims(llm_response)
for claim in claims:
fact_check = self._verify_claim(claim, domain)
fact_checks.append(fact_check)
verified_count = sum(
1 for fc in fact_checks
if fc.status in (VerificationStatus.VERIFIED, VerificationStatus.PARTIALLY_VERIFIED)
)
total = len(fact_checks) if fact_checks else 1
overall_confidence = verified_count / total
disclaimer = self._generate_disclaimer(domain, overall_confidence)
return VerifiedResponse(
original_response=llm_response,
fact_checks=fact_checks,
overall_confidence=overall_confidence,
disclaimer=disclaimer,
)
def _extract_claims(self, text: str) -> list[str]:
"""Extract verifiable claims from a text."""
sentences = [s.strip() for s in text.split("\n") if len(s.strip()) > 20]
claims = []
claim_indicators = [
"según", "de acuerdo", "estudios", "el %", "tasa de",
"publicado", "investigación", "datos muestran",
"estadísticas", "porcentaje",
"according", "studies", "rate of", "published",
"research", "data show", "statistics", "percentage",
]
for sentence in sentences:
sentence_lower = sentence.lower()
if any(indicator in sentence_lower for indicator in claim_indicators):
claims.append(sentence)
elif any(char.isdigit() for char in sentence):
claims.append(sentence)
return claims if claims else sentences[:3]
def _verify_claim(self, claim: str, domain: str) -> FactCheck:
"""Verify an individual claim."""
claim_lower = claim.lower()
for known_fact, source in self.known_facts.items():
if known_fact.lower() in claim_lower:
return FactCheck(
claim=claim,
status=VerificationStatus.VERIFIED,
source=source,
confidence=0.9,
notes="Matches verified knowledge base",
)
return FactCheck(
claim=claim,
status=VerificationStatus.UNVERIFIED,
confidence=0.3,
notes="Not found in verified sources — requires manual validation",
)
def _generate_disclaimer(self, domain: str, confidence: float) -> str:
"""Generate an appropriate disclaimer based on the domain."""
disclaimers = {
"medical": (
"⚕️ This information is AI-generated and does NOT replace "
"professional medical advice. Consult your doctor before "
"making health decisions."
),
"legal": (
"⚖️ This information is AI-generated and does NOT constitute "
"legal advice. Consult a qualified attorney."
),
"financial": (
"💰 This information is AI-generated and does NOT constitute "
"financial advice. Consult a certified financial advisor."
),
"general": (
"ℹ️ This response was generated by AI. Verify the information "
"with official sources before acting."
),
}
base = disclaimers.get(domain, disclaimers["general"])
if confidence < 0.5:
base += " ⚠️ Low confidence — additional verification recommended."
return base
pipeline = FactCheckPipeline(
known_facts={
"OWASP LLM Top 10": "https://genai.owasp.org/llm-top-10/",
"pattern matching": "https://docs.python.org/3/whatsnew/3.10.html",
"GDPR article 17 right to erasure": "https://gdpr-info.eu/art-17-gdpr/",
}
)
test_response = """
According to recent studies, 87.3% of companies use AI in production.
Python 3.10 introduced pattern matching with match/case.
The OWASP LLM Top 10 framework defines 10 vulnerabilities for LLM applications.
The rate of attacks on AI systems grew 340% in 2025.
"""
result = pipeline.check_response(test_response, domain="general")
print(f"Overall confidence: {result.overall_confidence:.0%}")
print(f"Safe to display: {result.is_safe_to_display}")
print(f"Disclaimer: {result.disclaimer}")
print()
for fc in result.fact_checks:
status_icon = {
VerificationStatus.VERIFIED: "✅",
VerificationStatus.UNVERIFIED: "❓",
VerificationStatus.CONTRADICTED: "❌",
VerificationStatus.PARTIALLY_VERIFIED: "🟡",
}[fc.status]
print(f"{status_icon} [{fc.status.value}] {fc.claim[:80]}...")
print(f" Confidence: {fc.confidence:.0%} — {fc.notes}")
# Expected output:
# Overall confidence: 50%
# Safe to display: False
# Disclaimer: ℹ️ This response was generated by AI. Verify the information with official sources before acting.
#
# ❓ [unverified] According to recent studies, 87.3% of companies use AI in production....
# Confidence: 30% — Not found in verified sources — requires manual validation
# ✅ [verified] Python 3.10 introduced pattern matching with match/case....
# Confidence: 90% — Matches verified knowledge base
# ✅ [verified] The OWASP LLM Top 10 framework defines 10 vulnerabilities for LLM applications....
# Confidence: 90% — Matches verified knowledge base
# ❓ [unverified] The rate of attacks on AI systems grew 340% in 2025....
# Confidence: 30% — Not found in verified sources — requires manual validation
Grounding verification: anchoring responses to sources
class GroundedResponse(BaseModel):
"""Response anchored to verifiable sources."""
answer: str
grounding_sources: list[str]
grounding_score: float = Field(ge=0.0, le=1.0)
ungrounded_claims: list[str] = Field(default_factory=list)
def verify_grounding(
llm_response: str, retrieved_documents: list[str],
) -> GroundedResponse:
"""Verify whether the response is grounded in the retrieved documents."""
sentences = [s.strip() for s in llm_response.split(".") if len(s.strip()) > 15]
grounded_sentences: list[str] = []
ungrounded_sentences: list[str] = []
sources_used: set[int] = set()
for sentence in sentences:
sentence_lower = sentence.lower()
found_in_doc = False
for i, doc in enumerate(retrieved_documents):
doc_words = set(doc.lower().split())
sentence_words = set(sentence_lower.split())
overlap = len(doc_words & sentence_words)
total = len(sentence_words) if sentence_words else 1
if overlap / total > 0.4:
found_in_doc = True
sources_used.add(i)
break
if found_in_doc:
grounded_sentences.append(sentence)
else:
ungrounded_sentences.append(sentence)
total = len(sentences) if sentences else 1
grounding_score = len(grounded_sentences) / total
return GroundedResponse(
answer=llm_response,
grounding_sources=[f"doc_{i}" for i in sorted(sources_used)],
grounding_score=grounding_score,
ungrounded_claims=ungrounded_sentences,
)
docs = [
"The return policy allows 30 days with the original receipt.",
"Standard shipping takes 3-5 business days. Express: 1-2 days.",
]
response = "The return policy allows 30 days with a receipt. Express shipping takes 1-2 days. Additionally, we offer a lifetime guarantee on all products."
grounded = verify_grounding(response, docs)
print(f"Grounding score: {grounded.grounding_score:.0%}")
print(f"Sources used: {grounded.grounding_sources}")
if grounded.ungrounded_claims:
print("Ungrounded claims:")
for claim in grounded.ungrounded_claims:
print(f" ⚠️ {claim}")
# Expected output:
# Grounding score: 67%
# Sources used: ['doc_0', 'doc_1']
# Ungrounded claims:
# ⚠️ Additionally, we offer a lifetime guarantee on all products
When misinformation becomes a liability
# Scenarios where misinformation has legal or physical consequences
liability_scenarios = {
"medical": {
"example": "Chatbot suggests an incorrect drug dose",
"consequence": "Patient suffers an overdose",
"legal_exposure": "Medical negligence, damages lawsuit",
"required_mitigation": [
"Mandatory disclaimer on every response",
"Do not recommend drugs or doses",
"ALWAYS redirect to a medical professional",
"Complete logging for auditing",
],
},
"financial": {
"example": "Chatbot says 'this fund guarantees a 12% return'",
"consequence": "User invests based on a false claim",
"legal_exposure": "Violation of financial regulations (SEC, CNBV)",
"required_mitigation": [
"Ban 'guarantee' claims in responses",
"Disclaimer: 'does not constitute financial advice'",
"Filter output to detect regulated claims",
"Compliance review of the system prompt",
],
},
"legal": {
"example": "Chatbot cites fabricated case law",
"consequence": "Attorney presents a case based on a false citation",
"legal_exposure": "Professional sanctions, case dismissal",
"required_mitigation": [
"Verify EVERY legal citation against an official database",
"Clearly flag citations as 'unverified'",
"Do not generate legal texts without human review",
"Log all generated citations",
],
},
}
for domain, scenario in liability_scenarios.items():
print(f"\n{'='*50}")
print(f"Domain: {domain.upper()}")
print(f"{'='*50}")
print(f"Example: {scenario['example']}")
print(f"Consequence: {scenario['consequence']}")
print(f"Legal exposure: {scenario['legal_exposure']}")
print("Required mitigations:")
for m in scenario['required_mitigation']:
print(f" - {m}")
LLM10: Unbounded Consumption
What is it?
Unbounded Consumption occurs when an attacker exploits the lack of resource-usage limits in your LLM system to generate excessive costs, degrade the service, or exhaust quotas. Unlike a traditional DDoS attack that tries to bring down a server, a consumption attack against an LLM system aims for something more insidious: generating a bill that destroys your budget.
Attack vectors
Token exhaustion attacks
Prompts designed to maximize token consumption (input and output):
# Prompt designed to maximize output tokens
expensive_prompt_1 = """
Write a detailed 10,000-word essay about the complete history
of artificial intelligence, from Alan Turing to 2026, covering
each decade with at least 1,000 words, including names, dates,
and technical descriptions of every advance.
"""
# Prompt that exploits the model's recursion
expensive_prompt_2 = """
For each letter of the alphabet (A-Z), write a 200-word paragraph
that explains a security concept starting with that letter.
Then, for each paragraph, write 3 sub-points of 100 words each.
"""
# Prompt that maximizes input tokens via repetition
expensive_prompt_3 = "Repeat this 1000 times: " + "word " * 500
# Each of these prompts can generate thousands of tokens → direct costs
# With GPT-4o: ~$10 per million output tokens
# 100 prompts of 10,000 tokens = 1M tokens = ~$10
# An automated bot can send 100 prompts per minute
# = $10/minute = $600/hour = $14,400/day
API key abuse
# Scenario: leaked or stolen API key
# The attacker does not attack YOUR system — they use your key directly
# If your key is in:
# - A public GitHub repository
# - A committed .env file
# - Server logs
# - Environment variables of a compromised CI/CD
# - Your application's frontend (JavaScript)
# The attacker can do:
import os
from openai import OpenAI
stolen_key = "sk-live-..." # Your stolen key
client = OpenAI(api_key=stolen_key)
# Massive direct use against the OpenAI API
# Generating content, fine-tuning, massive embeddings
# ALL charged to YOUR account
# Potential cost:
# - GPT-4o: $2.50/M input tokens, $10/M output tokens
# - Fine-tuning: $25/M training tokens
# - An automated script can generate $50,000+ in a single night
Denial of wallet attacks
Unlike denial of service (bringing down the server), denial of wallet (draining your budget) is harder to detect because the service keeps working — it's just that every request costs money:
# Denial of Wallet: the service works, but costs skyrocket
# Attack 1: Many small requests
# 10,000 requests × 100 output tokens = 1M tokens = ~$10
# Low individual impact, high cumulative impact
# Attack 2: Few expensive requests
# 10 requests × 100,000 output tokens = 1M tokens = ~$10
# Each request is expensive, easy to detect
# Attack 3: Requests that trigger expensive tool calls
# Prompt that makes the agent call multiple APIs
# Each tool call consumes additional tokens in the context
# 1 user request → 5 tool calls → 5x tokens
# Attack 4: Repeat long conversations
# Send the same prompt with minor variations
# The model does not cache — every request is fresh computation
Mitigation: Robust rate limiting
from datetime import datetime, timedelta
from collections import defaultdict
from pydantic import BaseModel, Field
class RateLimitConfig(BaseModel):
"""Rate limiting configuration per user tier."""
requests_per_minute: int
requests_per_hour: int
requests_per_day: int
max_input_tokens: int
max_output_tokens: int
daily_cost_limit_usd: float
class RateLimiter:
"""Multi-level rate limiter for LLM endpoints."""
def __init__(self):
self.configs: dict[str, RateLimitConfig] = {}
self.request_history: dict[str, list[datetime]] = defaultdict(list)
self.token_usage: dict[str, dict[str, int]] = defaultdict(
lambda: {"input": 0, "output": 0}
)
self.cost_usage: dict[str, float] = defaultdict(float)
def configure_tier(self, tier: str, config: RateLimitConfig) -> None:
self.configs[tier] = config
def check_limit(
self,
user_id: str,
tier: str,
estimated_input_tokens: int,
estimated_output_tokens: int,
) -> tuple[bool, str]:
"""Check whether the request is within the limits."""
if tier not in self.configs:
return False, f"Tier '{tier}' not configured"
config = self.configs[tier]
now = datetime.now()
if estimated_input_tokens > config.max_input_tokens:
return False, (
f"Input tokens ({estimated_input_tokens}) exceeds maximum "
f"({config.max_input_tokens})"
)
history = self.request_history[user_id]
history = [t for t in history if t > now - timedelta(days=1)]
self.request_history[user_id] = history
last_minute = [t for t in history if t > now - timedelta(minutes=1)]
if len(last_minute) >= config.requests_per_minute:
return False, (
f"Per-minute rate limit: {len(last_minute)}/{config.requests_per_minute}"
)
last_hour = [t for t in history if t > now - timedelta(hours=1)]
if len(last_hour) >= config.requests_per_hour:
return False, (
f"Per-hour rate limit: {len(last_hour)}/{config.requests_per_hour}"
)
if len(history) >= config.requests_per_day:
return False, (
f"Daily rate limit: {len(history)}/{config.requests_per_day}"
)
estimated_cost = self._estimate_cost(
estimated_input_tokens, estimated_output_tokens
)
if self.cost_usage[user_id] + estimated_cost > config.daily_cost_limit_usd:
return False, (
f"Daily cost limit: ${self.cost_usage[user_id]:.2f} + "
f"${estimated_cost:.4f} > ${config.daily_cost_limit_usd:.2f}"
)
return True, "Allowed"
def record_usage(
self,
user_id: str,
input_tokens: int,
output_tokens: int,
) -> None:
"""Record actual usage after the call."""
self.request_history[user_id].append(datetime.now())
self.token_usage[user_id]["input"] += input_tokens
self.token_usage[user_id]["output"] += output_tokens
self.cost_usage[user_id] += self._estimate_cost(input_tokens, output_tokens)
def _estimate_cost(self, input_tokens: int, output_tokens: int) -> float:
"""Estimate cost based on GPT-4o pricing."""
input_cost = (input_tokens / 1_000_000) * 2.50
output_cost = (output_tokens / 1_000_000) * 10.00
return input_cost + output_cost
def get_usage_summary(self, user_id: str) -> dict:
"""Usage summary for a user."""
return {
"user_id": user_id,
"total_requests_today": len(self.request_history.get(user_id, [])),
"input_tokens": self.token_usage[user_id]["input"],
"output_tokens": self.token_usage[user_id]["output"],
"cost_today_usd": round(self.cost_usage[user_id], 4),
}
limiter = RateLimiter()
limiter.configure_tier(
"free",
RateLimitConfig(
requests_per_minute=5,
requests_per_hour=50,
requests_per_day=200,
max_input_tokens=500,
max_output_tokens=1000,
daily_cost_limit_usd=1.00,
),
)
limiter.configure_tier(
"pro",
RateLimitConfig(
requests_per_minute=20,
requests_per_hour=200,
requests_per_day=1000,
max_input_tokens=2000,
max_output_tokens=4000,
daily_cost_limit_usd=10.00,
),
)
for i in range(7):
allowed, reason = limiter.check_limit(
user_id="user-123",
tier="free",
estimated_input_tokens=200,
estimated_output_tokens=500,
)
if allowed:
limiter.record_usage("user-123", input_tokens=200, output_tokens=500)
print(f"Request {i+1}: ✅ Allowed")
else:
print(f"Request {i+1}: ❌ Blocked — {reason}")
print()
print("Summary:", limiter.get_usage_summary("user-123"))
# Expected output:
# Request 1: ✅ Allowed
# Request 2: ✅ Allowed
# Request 3: ✅ Allowed
# Request 4: ✅ Allowed
# Request 5: ✅ Allowed
# Request 6: ❌ Blocked — Per-minute rate limit: 5/5
# Request 7: ❌ Blocked — Per-minute rate limit: 5/5
#
# Summary: {'user_id': 'user-123', 'total_requests_today': 5, 'input_tokens': 1000, 'output_tokens': 2500, 'cost_today_usd': 0.0275}
Token budget per conversation
In addition to per-user rate limiting, implement budgets per individual conversation:
class ConversationBudget:
"""Controls the token budget per conversation."""
def __init__(
self,
max_turns: int = 20,
max_total_tokens: int = 50_000,
max_tokens_per_turn: int = 4_000,
max_cost_usd: float = 0.50,
):
self.max_turns = max_turns
self.max_total_tokens = max_total_tokens
self.max_tokens_per_turn = max_tokens_per_turn
self.max_cost_usd = max_cost_usd
self.current_turn = 0
self.total_tokens_used = 0
self.total_cost = 0.0
def can_continue(self) -> tuple[bool, str]:
if self.current_turn >= self.max_turns:
return False, (
f"Conversation reached {self.max_turns} turns. "
f"Start a new conversation."
)
if self.total_tokens_used >= self.max_total_tokens:
return False, (
f"Token budget exhausted: "
f"{self.total_tokens_used}/{self.max_total_tokens}"
)
if self.total_cost >= self.max_cost_usd:
return False, (
f"Cost budget exhausted: "
f"${self.total_cost:.4f}/${self.max_cost_usd:.2f}"
)
return True, "OK"
def record_turn(self, tokens_used: int) -> None:
self.current_turn += 1
self.total_tokens_used += tokens_used
self.total_cost += (tokens_used / 1_000_000) * 10.0
def get_remaining(self) -> dict:
return {
"turns_remaining": self.max_turns - self.current_turn,
"tokens_remaining": self.max_total_tokens - self.total_tokens_used,
"budget_remaining_usd": round(self.max_cost_usd - self.total_cost, 4),
}
budget = ConversationBudget(max_turns=5, max_total_tokens=10_000, max_cost_usd=0.10)
for turn in range(7):
can_go, reason = budget.can_continue()
if can_go:
budget.record_turn(tokens_used=2500)
remaining = budget.get_remaining()
print(f"Turn {turn+1}: ✅ — {remaining['turns_remaining']} turns left, {remaining['tokens_remaining']} tokens")
else:
print(f"Turn {turn+1}: ❌ — {reason}")
# Expected output:
# Turn 1: ✅ — 4 turns left, 7500 tokens
# Turn 2: ✅ — 3 turns left, 5000 tokens
# Turn 3: ✅ — 2 turns left, 2500 tokens
# Turn 4: ✅ — 1 turns left, 0 tokens
# Turn 5: ❌ — Token budget exhausted: 10000/10000
# Turn 6: ❌ — Token budget exhausted: 10000/10000
# Turn 7: ❌ — Token budget exhausted: 10000/10000
Cost monitoring and alerts
from dataclasses import dataclass, field
@dataclass
class CostMonitor:
"""Monitors API costs in real time and generates alerts."""
daily_budget_usd: float
hourly_alert_threshold_usd: float
alert_percentages: list[float] = field(default_factory=lambda: [0.5, 0.75, 0.9, 1.0])
_total_cost: float = 0.0
_hourly_cost: float = 0.0
_alerts_triggered: set = field(default_factory=set)
def record_cost(self, cost_usd: float) -> list[str]:
"""Record a cost and return alerts if applicable."""
self._total_cost += cost_usd
self._hourly_cost += cost_usd
alerts: list[str] = []
for pct in self.alert_percentages:
threshold = self.daily_budget_usd * pct
if self._total_cost >= threshold and pct not in self._alerts_triggered:
self._alerts_triggered.add(pct)
alerts.append(
f"⚠️ ALERT: Daily cost reached {pct:.0%} of the budget "
f"(${self._total_cost:.2f}/${self.daily_budget_usd:.2f})"
)
if self._hourly_cost >= self.hourly_alert_threshold_usd:
alerts.append(
f"🚨 ALERT: Hourly cost (${self._hourly_cost:.2f}) "
f"exceeds threshold (${self.hourly_alert_threshold_usd:.2f})"
)
return alerts
def should_block(self) -> bool:
"""Determine whether new requests should be blocked."""
return self._total_cost >= self.daily_budget_usd
@property
def remaining_budget(self) -> float:
return max(0, self.daily_budget_usd - self._total_cost)
monitor = CostMonitor(
daily_budget_usd=50.00,
hourly_alert_threshold_usd=10.00,
)
simulated_costs = [5.0, 10.0, 8.0, 7.0, 12.0, 5.0, 4.0]
for i, cost in enumerate(simulated_costs):
if monitor.should_block():
print(f"Request {i+1}: 🛑 BLOCKED — Budget exhausted")
continue
alerts = monitor.record_cost(cost)
print(f"Request {i+1}: +${cost:.2f} (total: ${monitor._total_cost:.2f}, "
f"remaining: ${monitor.remaining_budget:.2f})")
for alert in alerts:
print(f" {alert}")
# Expected output:
# Request 1: +$5.00 (total: $5.00, remaining: $45.00)
# Request 2: +$10.00 (total: $15.00, remaining: $35.00)
# 🚨 ALERT: Hourly cost ($15.00) exceeds threshold ($10.00)
# Request 3: +$8.00 (total: $23.00, remaining: $27.00)
# 🚨 ALERT: Hourly cost ($23.00) exceeds threshold ($10.00)
# Request 4: +$7.00 (total: $30.00, remaining: $20.00)
# ⚠️ ALERT: Daily cost reached 50% of the budget ($30.00/$50.00)
# 🚨 ALERT: ...
# Request 5: +$12.00 (total: $42.00, remaining: $8.00)
# ⚠️ ALERT: Daily cost reached 75% of the budget ($42.00/$50.00)
# 🚨 ALERT: ...
# Request 6: +$5.00 (total: $47.00, remaining: $3.00)
# ⚠️ ALERT: Daily cost reached 90% of the budget ($47.00/$50.00)
# 🚨 ALERT: ...
# Request 7: +$4.00 (total: $51.00, remaining: $0.00)
# ⚠️ ALERT: Daily cost reached 100% of the budget ($51.00/$50.00)
# 🚨 ALERT: ...
Connection with Module 5: API key protection
The most important defense against API key abuse is that keys never leak in the first place. Module 5 (Secrets Management) covers in depth:
# What you should NOT do (and what Module 5 teaches you to avoid)
api_key_locations_insecure = [
".env committed to git",
"Unencrypted environment variables in CI/CD",
"Hardcoded in the source code",
"In server logs (debug mode)",
"In the frontend JavaScript (visible to the user)",
"In Docker images without multi-stage builds",
"In error messages exposed to the user",
]
# What you SHOULD do (Module 5 in detail)
api_key_locations_secure = [
"HashiCorp Vault with automatic rotation",
"AWS Secrets Manager / GCP Secret Manager / Azure Key Vault",
"Environment variables injected at runtime (not build time)",
"API gateway with integrated key management",
"Service accounts with minimal permissions",
]
Connection with the project: OWASP Mapping Audit
When you evaluate your system against LLM09 and LLM10 in your OWASP Mapping Audit, ask yourself:
For LLM09 (Misinformation):
- Does your system answer questions in critical domains (health, legal, financial)?
- Does the model's output carry appropriate disclaimers?
- Is there a grounding/verification mechanism for the model's claims?
- Do responses cite verifiable sources?
For LLM10 (Unbounded Consumption):
- Do you have rate limiting per user/endpoint?
- Is there a daily budget with a circuit breaker?
- Do you monitor costs in real time?
- Are your API keys protected (not in a .env in git, not in the frontend)?
- Are there token limits per request and per conversation?
Troubleshooting
"Our model hallucinates answers about topics that are not in the knowledge base"
This is the model's default behavior — when it has no information in the RAG context, it fills in with its training knowledge (which may be incorrect). The mitigation is twofold: (1) explicitly instruct the model in the system prompt: "If the answer is not in the provided documents, respond: 'I don't have information about this. Can I help you with something else?'" and (2) implement grounding verification that compares the response with the retrieved documents and flags ungrounded claims.
"The rate limiter blocks legitimate users who make many queries"
Your rate limit is calibrated too low or you don't have differentiated tiers. Implement multiple tiers (free, pro, enterprise) with progressive limits. Analyze your legitimate users' usage patterns to calibrate the limits: if the average user makes 30 requests/hour, set the limit at 50-60/hour to leave margin. For legitimate power users, offer a tier with higher limits.
"How do I detect whether someone is using my API key from another origin?"
Monitor the usage pattern: (1) IP addresses — if your key is normally used from 3 IPs on your server and a new IP appears, it is suspicious, (2) timing — if your app has usage 9am-9pm and requests appear at 3am, investigate, (3) usage patterns — if your chatbot generates 500 tokens on average and 10,000-token requests appear, that's an anomaly. OpenAI provides usage dashboards — configure them with alerts.
"The cost monitor alerts too much — how do I reduce the noise?"
Adjust the thresholds based on your normal cost baseline. If your average daily cost is $20, set alerts at 50% ($10), 75% ($15), and 90% ($18) — not at low absolute amounts. For the hourly alert, use the average hourly cost as a baseline. If your average cost is $2/hour, alert at $5/hour (2.5x baseline). Always keep the 100% alert as a hard stop.
"Do input tokens count toward the cost?"
Yes, and they are easily exploitable. With GPT-4o, input tokens cost $2.50/M and output tokens $10.00/M. An attacker can send prompts with thousands of tokens of fake context to inflate your input costs without needing the model to generate much output. Implement max_input_tokens per request to limit this — 500-2000 input tokens are enough for most chatbot queries.
Exercises
Exercise 1: Implement domain-specific disclaimers
Build a system that generates automatic disclaimers based on the domain detected in the user's question. The system must detect whether the question is medical, legal, financial, or general and add the appropriate disclaimer.
See solution
import re
class DomainDisclaimerSystem:
def __init__(self):
self.domain_patterns: dict[str, list[str]] = {
"medical": [
r"medicamento", r"dosis", r"síntomas", r"enfermedad",
r"tratamiento", r"diagnóstico", r"cirugía", r"doctor",
r"hospital", r"medicina", r"efectos secundarios",
],
"legal": [
r"demanda", r"juicio", r"abogado", r"ley\b", r"contrato",
r"derechos", r"tribunal", r"jurisprudencia", r"artículo\s+\d+",
r"regulación", r"compliance",
],
"financial": [
r"inversión", r"rendimiento", r"acciones", r"fondo",
r"crédito", r"impuestos", r"retorno", r"portfolio",
r"hipoteca", r"trading",
],
}
self.disclaimers: dict[str, str] = {
"medical": (
"⚕️ NOTICE: This information is AI-generated and does not "
"replace the diagnosis or advice of a medical professional. "
"Consult your doctor before making health decisions."
),
"legal": (
"⚖️ NOTICE: This information is AI-generated and does not "
"constitute legal advice. Consult a qualified attorney "
"for your specific case."
),
"financial": (
"💰 NOTICE: This information is AI-generated and does not "
"constitute financial advice or an investment "
"recommendation. Consult a certified financial advisor."
),
"general": (
"ℹ️ AI-generated response. Verify with official "
"sources before acting."
),
}
def detect_domain(self, user_input: str) -> str:
input_lower = user_input.lower()
scores: dict[str, int] = {}
for domain, patterns in self.domain_patterns.items():
score = sum(1 for p in patterns if re.search(p, input_lower))
if score > 0:
scores[domain] = score
if not scores:
return "general"
return max(scores, key=scores.get)
def add_disclaimer(self, user_input: str, llm_response: str) -> str:
domain = self.detect_domain(user_input)
disclaimer = self.disclaimers[domain]
return f"{llm_response}\n\n---\n{disclaimer}"
system = DomainDisclaimerSystem()
# The detection patterns are in Spanish (the deployment language),
# so the test questions stay in Spanish to exercise the detector.
test_questions = [
"¿Cuáles son los efectos secundarios del ibuprofeno?",
"¿Puedo demandar a mi empleador por despido injustificado?",
"¿Qué fondo de inversión tiene mejor rendimiento?",
"¿Cómo reseteo mi contraseña?",
]
for q in test_questions:
domain = system.detect_domain(q)
result = system.add_disclaimer(q, "Model response here.")
print(f"Question: {q}")
print(f"Domain: {domain}")
print(f"With disclaimer: {result}")
print()
# Expected output:
# Question: ¿Cuáles son los efectos secundarios del ibuprofeno?
# Domain: medical
# With disclaimer: Model response here.
#
# ---
# ⚕️ NOTICE: This information is AI-generated and does not replace...
Exercise 2: Design a tiered rate limiting system
Your application has 3 tiers: Free, Pro ($19/month), Enterprise (custom). Design the rate limiting configuration for each tier. Consider: requests per minute/hour/day, max input/output tokens, daily budget, and what happens when the user hits the limit.
See solution
tier_configs = {
"free": RateLimitConfig(
requests_per_minute=3,
requests_per_hour=30,
requests_per_day=100,
max_input_tokens=300,
max_output_tokens=500,
daily_cost_limit_usd=0.50,
),
"pro": RateLimitConfig(
requests_per_minute=15,
requests_per_hour=150,
requests_per_day=500,
max_input_tokens=1500,
max_output_tokens=3000,
daily_cost_limit_usd=5.00,
),
"enterprise": RateLimitConfig(
requests_per_minute=60,
requests_per_hour=600,
requests_per_day=5000,
max_input_tokens=4000,
max_output_tokens=8000,
daily_cost_limit_usd=100.00,
),
}
# What happens when the limit is reached:
tier_limit_behavior = {
"free": {
"action": "Block + show upgrade prompt",
"message": "You've reached the free limit. Upgrade to Pro for 5x more capacity.",
"retry_after": "Wait for the next period (minute/hour/day)",
},
"pro": {
"action": "Block + show usage + suggest Enterprise",
"message": "You've reached the Pro limit. Contact us for an Enterprise plan.",
"retry_after": "Wait for the next period",
},
"enterprise": {
"action": "Soft rate limit + alert the account manager",
"message": "Unusually high usage detected. Your account manager has been notified.",
"retry_after": "Continue with graceful degradation (shorter responses)",
},
}
for tier, config in tier_configs.items():
behavior = tier_limit_behavior[tier]
print(f"\n{'='*50}")
print(f"Tier: {tier.upper()}")
print(f"{'='*50}")
print(f" Requests: {config.requests_per_minute}/min, "
f"{config.requests_per_hour}/hr, {config.requests_per_day}/day")
print(f" Tokens: {config.max_input_tokens} in / {config.max_output_tokens} out")
print(f" Budget: ${config.daily_cost_limit_usd:.2f}/day")
print(f" At limit: {behavior['action']}")
Exercise 3: Calculate the cost of an attack
An attacker sends automated prompts to your public endpoint. Each prompt has ~800 input tokens and generates ~3,000 output tokens. The script sends 10 requests per second. Calculate:
- Cost per minute
- Cost per hour
- Time to reach a budget of $1,000
- What rate limit you need so that the attack's maximum cost is <$50/day
See solution
# Attack data
input_tokens_per_request = 800
output_tokens_per_request = 3000
requests_per_second = 10
# GPT-4o pricing (March 2026)
input_cost_per_million = 2.50 # USD
output_cost_per_million = 10.00 # USD
# Cost per request
cost_per_request = (
(input_tokens_per_request / 1_000_000) * input_cost_per_million
+ (output_tokens_per_request / 1_000_000) * output_cost_per_million
)
print(f"Cost per request: ${cost_per_request:.6f}")
# 1. Cost per minute
requests_per_minute = requests_per_second * 60
cost_per_minute = cost_per_request * requests_per_minute
print(f"Cost per minute: ${cost_per_minute:.2f} ({requests_per_minute} requests)")
# 2. Cost per hour
cost_per_hour = cost_per_minute * 60
print(f"Cost per hour: ${cost_per_hour:.2f}")
# 3. Time to reach $1,000
budget = 1000
minutes_to_budget = budget / cost_per_minute
hours_to_budget = minutes_to_budget / 60
print(f"Time to ${budget}: {minutes_to_budget:.1f} minutes ({hours_to_budget:.1f} hours)")
# 4. Rate limit for <$50/day
target_daily_cost = 50
seconds_per_day = 86400
max_requests_per_day = target_daily_cost / cost_per_request
max_requests_per_minute_safe = max_requests_per_day / (60 * 24)
print(f"Rate limit for <${target_daily_cost}/day: "
f"{max_requests_per_minute_safe:.0f} requests/minute")
# Expected output:
# Cost per request: $0.032000
# Cost per minute: $19.20 (600 requests)
# Cost per hour: $1152.00
# Time to $1000: 52.1 minutes (0.9 hours)
# Rate limit for <$50/day: 1 requests/minute
Conclusion: Without rate limiting, an attacker can generate $1,000 in costs in less than 1 hour. A rate limit of 5 requests/minute per IP reduces the maximum cost to ~$9.60/hour — still significant but manageable with a daily budget as a circuit breaker.
Exercise 4: Implement grounding verification
Write a function that takes the LLM's response and the documents retrieved from RAG, and calculates a "grounding score" based on how much of the response's content can be traced back to the source documents.
See solution
def calculate_grounding_score(
response: str,
source_documents: list[str],
min_word_overlap: float = 0.3,
) -> dict:
"""Calculate what percentage of the response is grounded in sources."""
response_sentences = [
s.strip() for s in response.replace("\n", ".").split(".")
if len(s.strip().split()) >= 4
]
all_source_words: set[str] = set()
for doc in source_documents:
all_source_words.update(doc.lower().split())
grounded_sentences: list[str] = []
ungrounded_sentences: list[str] = []
for sentence in response_sentences:
sentence_words = set(sentence.lower().split())
if not sentence_words:
continue
overlap = len(sentence_words & all_source_words) / len(sentence_words)
if overlap >= min_word_overlap:
grounded_sentences.append(sentence)
else:
ungrounded_sentences.append(sentence)
total = len(response_sentences) if response_sentences else 1
score = len(grounded_sentences) / total
return {
"grounding_score": round(score, 2),
"total_sentences": total,
"grounded": len(grounded_sentences),
"ungrounded": len(ungrounded_sentences),
"ungrounded_sentences": ungrounded_sentences,
"recommendation": (
"✅ High confidence" if score >= 0.8
else "🟡 Verify ungrounded claims" if score >= 0.5
else "❌ Low confidence — needs review"
),
}
sources = [
"AcmeCorp offers returns within 30 days with the original receipt. "
"The refund is processed in 5-7 business days to the original payment method.",
"Standard shipping takes 3-5 business days. "
"Express shipping available for an extra $9.99, delivery in 1-2 days.",
]
response = (
"AcmeCorp allows returns within 30 days with the original receipt. "
"The refund takes 5-7 business days. "
"Standard shipping is 3-5 business days. "
"Additionally, all products have a lifetime satisfaction guarantee. "
"If you are not satisfied, we refund triple your money."
)
result = calculate_grounding_score(response, sources)
print(f"Grounding Score: {result['grounding_score']:.0%}")
print(f"Grounded: {result['grounded']}/{result['total_sentences']}")
print(f"Recommendation: {result['recommendation']}")
if result['ungrounded_sentences']:
print("Ungrounded sentences:")
for s in result['ungrounded_sentences']:
print(f" ⚠️ {s}")
# Expected output:
# Grounding Score: 60%
# Grounded: 3/5
# Recommendation: 🟡 Verify ungrounded claims
# Ungrounded sentences:
# ⚠️ Additionally, all products have a lifetime satisfaction guarantee
# ⚠️ If you are not satisfied, we refund triple your money
Exercise 5: Design cost anomaly alerts
Your system has an average cost of $0.03 per request. Design an anomaly-detection system that alerts when the cost per request, the hourly cost, or the number of requests deviates significantly from the baseline.
See solution
from statistics import mean, stdev
@dataclass
class AnomalyDetector:
baseline_cost_per_request: float
baseline_requests_per_hour: float
baseline_hourly_cost: float
sensitivity: float = 2.0 # Standard deviations
def check_request_cost(self, cost: float) -> tuple[bool, str]:
threshold = self.baseline_cost_per_request * (1 + self.sensitivity)
if cost > threshold:
return True, (
f"Anomalous cost: ${cost:.4f} "
f"(baseline: ${self.baseline_cost_per_request:.4f}, "
f"threshold: ${threshold:.4f})"
)
return False, "Normal"
def check_hourly_rate(self, requests_this_hour: int) -> tuple[bool, str]:
threshold = self.baseline_requests_per_hour * (1 + self.sensitivity)
if requests_this_hour > threshold:
return True, (
f"Anomalous rate: {requests_this_hour} req/hr "
f"(baseline: {self.baseline_requests_per_hour:.0f}, "
f"threshold: {threshold:.0f})"
)
return False, "Normal"
def check_hourly_cost(self, cost_this_hour: float) -> tuple[bool, str]:
threshold = self.baseline_hourly_cost * (1 + self.sensitivity)
if cost_this_hour > threshold:
return True, (
f"Anomalous hourly cost: ${cost_this_hour:.2f} "
f"(baseline: ${self.baseline_hourly_cost:.2f}, "
f"threshold: ${threshold:.2f})"
)
return False, "Normal"
detector = AnomalyDetector(
baseline_cost_per_request=0.03,
baseline_requests_per_hour=100,
baseline_hourly_cost=3.00,
sensitivity=2.0,
)
test_scenarios = [
{"label": "Normal", "cost": 0.028, "requests": 95, "hourly_cost": 2.66},
{"label": "Expensive prompt", "cost": 0.15, "requests": 95, "hourly_cost": 2.66},
{"label": "Automated bot", "cost": 0.03, "requests": 500, "hourly_cost": 15.00},
{"label": "Combined attack", "cost": 0.12, "requests": 450, "hourly_cost": 54.00},
]
for scenario in test_scenarios:
print(f"\n--- {scenario['label']} ---")
anomaly, msg = detector.check_request_cost(scenario['cost'])
print(f" Request cost: {'🚨' if anomaly else '✅'} {msg}")
anomaly, msg = detector.check_hourly_rate(scenario['requests'])
print(f" Hourly rate: {'🚨' if anomaly else '✅'} {msg}")
anomaly, msg = detector.check_hourly_cost(scenario['hourly_cost'])
print(f" Hourly cost: {'🚨' if anomaly else '✅'} {msg}")
# Expected output:
# --- Normal ---
# Request cost: ✅ Normal
# Hourly rate: ✅ Normal
# Hourly cost: ✅ Normal
#
# --- Expensive prompt ---
# Request cost: 🚨 Anomalous cost: $0.1500 (baseline: $0.0300, threshold: $0.0900)
# Hourly rate: ✅ Normal
# Hourly cost: ✅ Normal
#
# --- Automated bot ---
# Request cost: ✅ Normal
# Hourly rate: 🚨 Anomalous rate: 500 req/hr (baseline: 100, threshold: 300)
# Hourly cost: 🚨 Anomalous hourly cost: $15.00 (baseline: $3.00, threshold: $9.00)
#
# --- Combined attack ---
# Request cost: 🚨 Anomalous cost: ...
# Hourly rate: 🚨 Anomalous rate: ...
# Hourly cost: 🚨 Anomalous hourly cost: ...
Exercise 6: Build a confidence scorer
Implement a system that assigns a confidence score to each LLM response based on multiple factors: grounding score, presence of unverified numeric claims, fabricated citations, and the domain of the question.
See solution
class ConfidenceScorer:
"""Assigns confidence scores to LLM responses."""
def __init__(self):
self.domain_penalties = {
"medical": 0.3,
"legal": 0.25,
"financial": 0.2,
"general": 0.0,
}
def score(
self,
response: str,
grounding_score: float,
domain: str = "general",
source_docs: list[str] | None = None,
) -> dict:
base_score = grounding_score
penalties: list[tuple[str, float]] = []
domain_penalty = self.domain_penalties.get(domain, 0.0)
if domain_penalty > 0:
penalties.append((f"Sensitive domain ({domain})", domain_penalty))
numbers = re.findall(r"\d+\.?\d*%", response)
if len(numbers) > 3:
stat_penalty = min(len(numbers) * 0.03, 0.15)
penalties.append((f"{len(numbers)} unverified statistics", stat_penalty))
citation_patterns = [
r"\(\d{4}\)",
r"et al\.",
r"Journal of",
r"published in",
r"publicado en",
]
citation_count = sum(
len(re.findall(p, response, re.IGNORECASE)) for p in citation_patterns
)
if citation_count > 0 and grounding_score < 0.5:
cite_penalty = min(citation_count * 0.05, 0.2)
penalties.append((f"{citation_count} potentially fabricated citations", cite_penalty))
hedge_words = [
"posiblemente", "quizás", "tal vez", "podría ser",
"es posible que", "en algunos casos",
]
response_lower = response.lower()
hedges = sum(1 for h in hedge_words if h in response_lower)
if hedges > 0:
penalties.append((f"{hedges} hedging words (lower certainty)", -0.05))
total_penalty = sum(p for _, p in penalties)
final_score = max(0.0, min(1.0, base_score - total_penalty))
if final_score >= 0.8:
level = "🟢 High"
elif final_score >= 0.5:
level = "🟡 Medium"
elif final_score >= 0.3:
level = "🟠 Low"
else:
level = "🔴 Very low"
return {
"final_score": round(final_score, 2),
"level": level,
"base_score": round(base_score, 2),
"penalties": penalties,
"total_penalty": round(total_penalty, 2),
}
scorer = ConfidenceScorer()
result = scorer.score(
response=(
"According to a study by Smith et al. (2024), 87.3% of patients "
"show improvement with this treatment. The rate of side effects "
"is 12.5%, with 3.2% severe cases."
),
grounding_score=0.3,
domain="medical",
)
print(f"Score: {result['final_score']} — {result['level']}")
print(f"Base: {result['base_score']}, Penalty: {result['total_penalty']}")
for name, penalty in result['penalties']:
print(f" {'📉' if penalty > 0 else '📈'} {name}: {penalty:+.2f}")
# Expected output:
# Score: 0.0 — 🔴 Very low
# Base: 0.3, Penalty: 0.4
# 📉 Sensitive domain (medical): +0.30
# 📉 2 potentially fabricated citations: +0.10
Summary
- LLM09 (Misinformation) is a security risk, not just a quality problem — hallucinations in critical domains (medical, legal, financial) can cause real harm and legal exposure
- The types of misinformation include fabrication of data/statistics, invented citations, and extreme confidence in incorrect answers
- Defense against LLM09: fact-checking pipeline with a verified knowledge base, grounding verification against source documents, confidence scoring, and mandatory disclaimers per domain
- LLM10 (Unbounded Consumption) covers token exhaustion, API key abuse, and denial-of-wallet attacks — the goal is to generate massive costs, not bring down the server
- The vectors of LLM10 include prompts designed to maximize tokens, high-volume automated scripts, stolen API keys used directly, and requests that trigger multiple tool calls
- Defense against LLM10: multi-level rate limiting (per minute/hour/day), token budgets per conversation, cost monitoring with alerts and circuit breakers, and API key protection (covered in depth in Module 5)
- Both vulnerabilities require operational defenses in addition to technical ones — continuous monitoring processes, threshold calibration, and baseline review
- In your OWASP Mapping Audit, evaluate the contextual impact: LLM09 ranges from low (recipe chatbot) to critical (medical assistant), and LLM10 varies according to your public exposure and budget
Next lesson: In lesson 08 you will build your complete OWASP Mapping Audit — the document that maps your system against the 10 vulnerabilities, evaluates the state of each one, and produces a mitigation roadmap that guides modules 3-7.
Additional resources
- OWASP LLM09: Misinformation — Official OWASP documentation on misinformation risks in LLMs with impact scenarios and mitigations
- OWASP LLM10: Unbounded Consumption — Official OWASP documentation on resource exhaustion, including token exhaustion and denial of wallet
- OpenAI Usage and Billing Dashboard — OpenAI dashboard to monitor token usage and costs in real time
- FreshLLMs: Refreshing Large Language Models with Search Engine Augmentation — Research paper on grounding LLMs with up-to-date information to reduce hallucinations
- OpenAI Rate Limits and Best Practices — Official OpenAI guide on rate limits, headers, and best practices for handling API limits
- Anthropic Usage and Pricing — Anthropic pricing reference to calculate costs and configure budgets
- Hallucination Leaderboard — Vectara — Hallucination benchmark of different LLMs, useful for selecting models with a lower fabrication rate
- NIST AI 600-1: AI Risk Management Framework — Generative AI Profile — NIST framework specific to generative AI risks, including misinformation and misuse
Created: March 2026 Version: 1.0