How Usage and Cost Tracking Works Per User and Per Session in Claude Code Telegram
The bot implements a three-layer tracking system using a RateLimiter for per-user token buckets and daily cost budgets, a Session model for per-session accumulation, and a StorageFacade for persistent SQLite records of all costs.
The RichardAtCT/claude-code-telegram repository provides a Telegram bot interface for Claude AI that requires strict accounting of API expenses. The system tracks every token request and USD-equivalent cost at both the user and session level, enabling real-time budget enforcement, daily resets, and comprehensive reporting for bot operators.
Per-User Cost Tracking via RateLimiter
The RateLimiter class in src/security/rate_limiter.py serves as the primary gatekeeper for per-user usage and cost tracking per user or per session. It maintains two critical data structures: a token bucket for request throttling and a cost tracker for budget enforcement.
Token-Bucket and Cost Budget Enforcement
The rate limiter initializes a cost_tracker: Dict[int, float] that maps Telegram user IDs to their cumulative USD spend within the current budgeting window. When a request arrives, the async method check_rate_limit(user_id, cost, tokens) validates both token availability and budget capacity.
After passing token-bucket checks, the system records the cost via _track_cost():
# src/security/rate_limiter.py – line 69
def _track_cost(self, user_id: int, cost: float) -> None:
self.cost_tracker[user_id] += cost
logger.debug("Cost tracked", user_id=user_id, cost=cost,
total_usage=self.cost_tracker[user_id])
Daily Budget Reset Mechanism
The _maybe_reset_cost_tracker() method (lines 94-104) automatically resets user budgets every 24 hours. It compares the current time against cost_reset_time[user_id] and clears the tracker when the interval expires:
# src/security/rate_limiter.py – line 95
reset_interval = timedelta(hours=24)
if now - last_reset >= reset_interval:
self.cost_tracker[user_id] = 0
self.cost_reset_time[user_id] = now
User Status Retrieval
The get_user_status(user_id) method (lines 30-48) returns a comprehensive status dump including a cost_usage dictionary with current, limit, remaining, and utilization fields. This powers the /status bot command and admin dashboards.
Per-Session Cost Accumulation
While the RateLimiter handles real-time enforcement, the Session model in src/claude/session.py maintains the cumulative cost of individual Claude conversations.
Session Model and Total Cost Field
The Session dataclass declares total_cost: float = 0.0 as a persistent field. This value represents the sum of all Claude API calls within that specific session window.
Real-Time Cost Updates
Whenever the Claude SDK returns a response, the session updates its running total:
# src/claude/session.py – line 64
self.total_cost += response.cost
The response.cost value originates from src/claude/sdk_integration.py, which extracts the total_cost_usd field from Claude's API response metadata. When the session object is persisted via src/storage/session_storage.py (lines 73-84), this total_cost is written to the SQLite sessions table.
Persistent Storage and Aggregation
The StorageFacade in src/storage/facade.py provides the durable layer for usage and cost tracking per user or per session, ensuring data survives bot restarts.
Storage Facade Pattern
After a Claude call completes, the Facade updates both user and session records atomically:
# src/storage/facade.py – lines 118-126
user.total_cost += response.cost
session.total_cost += response.cost
await self._update_user(user)
await self._update_session(session)
Database Schema
The underlying SQLite schema, defined in src/storage/database.py (lines 43-55), creates total_cost REAL DEFAULT 0.0 columns for the users, sessions, and messages tables. The User and Session ORM models in src/storage/models.py (lines 38 & 72) map these columns to Python attributes.
Aggregated Reporting
For administrative oversight, StorageFacade.dashboard() (line 321) aggregates per-session totals into system-wide statistics. Additionally, Repositories.get_total_costs(days) (line 654) in src/storage/repositories.py sums daily costs across all users for historical trend analysis, powering cost alerts and budget forecasting.
Practical Implementation Examples
Checking User Cost Usage Inside a Handler
# Example: inside a Telegram handler
from src.security.rate_limiter import RateLimiter
from src.config.settings import Settings
settings = Settings() # loads env vars
rate_limiter = RateLimiter(settings)
async def handle_message(update, context):
user_id = update.effective_user.id
# Estimate the cost of the incoming message (≈ 0.01 USD)
cost_estimate = 0.01
allowed, msg = await rate_limiter.check_rate_limit(user_id,
cost=cost_estimate,
tokens=1)
if not allowed:
await update.message.reply_text(msg) # informs about budget
return
# ... proceed with Claude call
Adding Cost to a Session After a Claude Response
from src.claude.session import Session
from src.claude.sdk_integration import ClaudeSDKManager
async def ask_claude(user_id, prompt):
session = await Session.load(user_id) # restores or creates
sdk = ClaudeSDKManager()
response = await sdk.query(prompt,
session_id=session.session_id)
# response.cost comes from Claude’s total_cost_usd field
session.total_cost += response.cost
await session.save() # persists total_cost
return response.text
Fetching User-Level Cost Summary for Admin Console
from src.storage.facade import StorageFacade
async def admin_cost_report():
facade = StorageFacade()
dashboard = await facade.dashboard()
total = dashboard["total_cost"] # overall system cost
per_user = dashboard["stats"]["summary"]["total_cost"]
return f"System spent ${total:.2f} total; current user spend ${per_user:.2f}"
Summary
- RateLimiter enforces per-user cost budgets and token buckets in
src/security/rate_limiter.py, automatically resetting daily limits via_maybe_reset_cost_tracker(). - Session objects accumulate conversation costs in real-time through the
total_costfield insrc/claude/session.py, updated after every Claude SDK response. - StorageFacade persists cumulative costs to SQLite via
src/storage/facade.py, updating bothUser.total_costandSession.total_costatomically after each API call. - Database schema in
src/storage/database.pydefinestotal_cost REALcolumns for users, sessions, and messages, ensuring durability across bot restarts. - Administrative reporting via
StorageFacade.dashboard()andRepositories.get_total_costs()enables system-wide cost monitoring and historical trend analysis.
Frequently Asked Questions
How does the system reset daily cost budgets for users?
The RateLimiter class tracks reset timestamps in cost_reset_time and automatically clears the cost_tracker dictionary for a user when _maybe_reset_cost_tracker() detects that 24 hours have elapsed since the last reset. This logic runs during every rate limit check in src/security/rate_limiter.py.
What happens when a user exceeds their cost limit?
When check_rate_limit() detects that the user's cumulative cost exceeds the configured budget, it returns (False, message) where the message explains the budget exhaustion. The Telegram handler in src/bot/handlers/message.py receives this rejection and replies to the user with the budget warning, blocking the Claude API call.
How is cost data stored persistently across bot restarts?
The StorageFacade in src/storage/facade.py writes cost updates to SQLite via the _update_user() and _update_session() methods. The underlying schema in src/storage/database.py defines total_cost REAL DEFAULT 0.0 columns in the users, sessions, and messages tables, ensuring that cumulative costs survive process restarts and can be queried historically via Repositories.get_total_costs().
Where does the per-session cost value originate?
Each Claude API response includes a total_cost_usd field that the ClaudeSDKManager in src/claude/sdk_integration.py extracts and maps to response.cost. The Session object in src/claude/session.py then adds this value to its total_cost field, creating a running tally of the conversation's API expenses that persists until the session ends.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →