11 KiB
Billing System
Overview
The Billing System provides persistent tracking of API usage and costs across all sessions. It records every LLM API call with token counts and calculated costs, storing them in monthly JSONL files for easy querying and analysis.
Design Principles
- Non-blocking — Billing persistence is fire-and-forget; failures never interrupt agent flow
- Minimal footprint — JSONL format, one file per month, no database dependency
- Privacy-first — Data stored locally in
~/.clacky/billing/, never uploaded - Accurate costing — Uses the same
ModelPricingmodule as real-time display
Architecture
┌─────────────────────────────────────────────────────────────────┐
│ Agent │
│ CostTracker module │
│ └── track_cost() │
│ ├── Calculate cost (ModelPricing) │
│ ├── Update UI (real-time) │
│ └── persist_billing_record() ──────┐ │
└─────────────────────────────────────────────┼───────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ Billing Module │
│ lib/clacky/billing/ │
│ ├── billing_record.rb (data structure) │
│ └── billing_store.rb (JSONL persistence) │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ Storage │
│ ~/.clacky/billing/ │
│ ├── 2026-05.jsonl │
│ ├── 2026-04.jsonl │
│ └── ... │
└─────────────────────────────────────────────────────────────────┘
Components
BillingRecord (lib/clacky/billing/billing_record.rb)
A Struct representing a single API call:
| Field | Type | Description |
|---|---|---|
id |
String | UUID, auto-generated |
session_id |
String | Associated session |
timestamp |
Time | When the call was made |
model |
String | Model name (e.g., "claude-sonnet-4.5") |
prompt_tokens |
Integer | Input tokens |
completion_tokens |
Integer | Output tokens |
cache_read_tokens |
Integer | Tokens read from cache |
cache_write_tokens |
Integer | Tokens written to cache |
cost_usd |
Float | Calculated cost in USD |
cost_source |
Symbol | :api, :price, or :estimated |
BillingStore (lib/clacky/billing/billing_store.rb)
Handles persistence and querying:
store = Clacky::Billing::BillingStore.new
# Append a record
store.append(record)
# Query with filters
records = store.query(from: 1.week.ago, model: "claude-sonnet-4.5", limit: 100)
# Get summary statistics
summary = store.summary(period: :month)
# => { total_cost: 12.34, total_tokens: 500000, by_model: {...}, ... }
# Daily breakdown for charts
daily = store.daily_breakdown(days: 30)
# => [{ date: "2026-05-01", cost: 1.23, tokens: 50000, requests: 42 }, ...]
Storage Format
Records are stored as JSON Lines (one JSON object per line):
{"id":"abc123","session_id":"def456","timestamp":"2026-05-22T15:30:00+08:00","model":"claude-sonnet-4.5","prompt_tokens":1500,"completion_tokens":500,"cache_read_tokens":1000,"cache_write_tokens":0,"cost_usd":0.0045,"cost_source":"price"}
{"id":"abc124","session_id":"def456","timestamp":"2026-05-22T15:31:00+08:00","model":"claude-sonnet-4.5","prompt_tokens":2000,"completion_tokens":800,"cache_read_tokens":1500,"cache_write_tokens":0,"cost_usd":0.0052,"cost_source":"price"}
Why JSONL?
- Append-only writes (no file locking needed)
- Easy to parse line-by-line (memory efficient)
- Human-readable for debugging
- Simple monthly rotation
API Endpoints
GET /api/billing/summary
Returns aggregated statistics for a time period.
Query Parameters:
period—day,week,month,year, orall(default:month)
Response:
{
"period": "month",
"from": "2026-05-01T00:00:00+08:00",
"to": "2026-05-22T15:30:00+08:00",
"total_cost": 12.3456,
"total_tokens": 500000,
"prompt_tokens": 350000,
"completion_tokens": 150000,
"cache_read_tokens": 200000,
"cache_write_tokens": 50000,
"by_model": {
"claude-sonnet-4.5": { "cost": 10.00, "requests": 100 },
"deepseek-v4-flash": { "cost": 2.34, "requests": 50 }
},
"by_day": {
"2026-05-22": 1.23,
"2026-05-21": 2.34
},
"record_count": 150
}
GET /api/billing/daily
Returns daily cost breakdown for charting.
Query Parameters:
days— Number of days (default: 30, max: 90)
Response:
{
"days": [
{ "date": "2026-05-22", "cost": 1.2345, "tokens": 50000, "requests": 42 },
{ "date": "2026-05-21", "cost": 2.3456, "tokens": 80000, "requests": 65 }
]
}
GET /api/billing/records
Returns raw billing records.
Query Parameters:
limit— Max records (default: 100, max: 500)model— Filter by model namesession_id— Filter by session ID
Response:
{
"records": [
{ "id": "...", "timestamp": "...", "model": "...", "cost_usd": 0.01, ... }
],
"count": 100
}
CLI Command
# Show current month's billing
clacky billing
# Show specific period
clacky billing --period week
clacky billing --period day
clacky billing --period all
# Output as JSON (for scripting)
clacky billing --json
Sample Output:
📊 Billing Summary (month)
──────────────────────────────────────────────────
💰 Total Cost: $12.3456
📝 Total Tokens: 500,000
📥 Prompt Tokens: 350,000
📤 Completion: 150,000
🗄️ Cache Read: 200,000
📝 Cache Write: 50,000
🔢 API Requests: 150
📈 By Model:
──────────────────────────────────────────────────
claude-sonnet-4.5
Cost: $10.0000 | Requests: 100
deepseek-v4-flash
Cost: $2.3456 | Requests: 50
📅 Recent Daily Usage:
──────────────────────────────────────────────────
2026-05-22 $1.2345 ████████████
2026-05-21 $2.3456 ████████████████████████
──────────────────────────────────────────────────
Data stored in: ~/.clacky/billing/
Web UI
The Billing panel is accessible from the sidebar under "My Data":
- Summary cards — Total cost, tokens, API requests
- Token breakdown — Prompt, completion, cache read/write
- By Model table — Cost and request count per model
- Daily chart — Visual bar chart of recent usage with detailed tooltips
- Period selector — Filter by day/week/month/year/all
Daily Chart Tooltips
Hover over any bar in the daily chart to see detailed information:
- Date and total cost
- Input tokens (prompt)
- Output tokens (completion)
- Cache read tokens with hit rate percentage
- Cache write tokens
- Number of API requests
Currency Settings
The Web UI supports multiple currencies for cost display:
| Currency | Symbol | Default Exchange Rate |
|---|---|---|
| USD | $ | 1.0 (base) |
| CNY | ¥ | 6.7944 (customizable) |
Configuration
- Go to Settings page
- Find the Currency section
- Select
$ USDor¥ CNY - When CNY is selected, you can customize the exchange rate
Custom Exchange Rate
When CNY is selected, an exchange rate input field appears:
- Default rate: 6.7944 (1 USD = 6.7944 CNY)
- Enter any positive number to customize
- Changes take effect immediately
- Rate is saved to browser localStorage
Scope
Currency settings apply to:
- Billing panel (total cost, model costs, daily chart)
- Session info bar (top cost display)
- Token usage lines (per-API-call cost)
- Task completion messages
Note: CLI always displays costs in USD (API's native currency).
Implementation
Currency preference is stored in browser localStorage:
clacky-currency: Currency code ("USD" or "CNY")clacky-exchange-rate: Custom exchange rate (number)
// Access currency utilities from Billing module
Billing.getCurrency() // "USD" or "CNY"
Billing.getCurrencySymbol() // "$" or "¥"
Billing.convertCost(usd) // Convert USD to selected currency
Billing.getExchangeRate() // Get current exchange rate
Integration with CostTracker
The billing system hooks into Agent::CostTracker#track_cost:
def track_cost(usage, raw_api_usage: nil)
# ... existing cost calculation ...
# Persist billing record (skip for subagents to avoid double-counting)
unless @is_subagent
persist_billing_record(usage, iteration_cost)
end
token_data
end
Key behaviors:
- Subagent costs are NOT recorded separately (parent agent merges them)
- Unknown model costs (nil) are skipped
- Persistence failures are logged but never raise
Data Retention
- Records are stored indefinitely by default
- Monthly files can be manually deleted from
~/.clacky/billing/ - Future:
BillingStore#cleanup(before: 1.year.ago)for automated retention
Future Enhancements
- Export to CSV/JSON
- Budget alerts (daily/monthly limits)
- Cost comparison across models
- Session-level cost breakdown in UI
- i18n support for billing labels (English/Chinese)
- Currency settings (USD/CNY)
- Dynamic exchange rate updates
- More currency options (EUR, JPY, etc.)