Usage Tracking: Provider Quota, Spend, and Session Fallbacks
Learn how usage tracking fetches provider quota and spend data, normalizes quota windows, and falls back to session logs when live data is missing. Essential for developers and admins monitoring resource consumption.
Read this when
- You are wiring provider usage/quota surfaces
- You need to explain usage tracking behavior or auth requirements
What it is
- Provider usage and quota details are fetched directly from each provider's usage endpoint. There is no estimated billing from the provider side; only plan names, quota windows, balances, spend, budgets, daily cost history, token/model attribution, or account-state summaries that the provider itself reports are included.
- Output for quota windows is presented in a human-readable format normalized to
X% left, even when the provider supplies consumed quota, remaining quota, or just raw counts. If a provider lacks a resettable quota window, summary text from the provider (such as a balance) is shown instead. - When the live session snapshot is missing token or model data, the session-level
/statusand thesession_statustool fall back to the transcript log of that session. This fallback can fill in missing token/cache counters, may recover the active runtime model label, and prefers the larger prompt-oriented total when session metadata is absent or smaller (totalTokensFresh !== true, zero, or below the transcript-derived value). Nonzero live values always take precedence over the fallback.
Where it shows up
/statusin chats: a status card showing session tokens and estimated cost (only for API key models). Provider usage appears for the current model provider when it is available, as a normalizedX% leftwindow or provider summary text./usage off|tokens|fullin chats: a usage footer for each response./usage costin chats: a local cost summary aggregated from OpenClaw session logs.- CLI:
openclaw status --usageoutputs a complete per-provider usage/quota breakdown. - CLI:
openclaw models statuslists OAuth/token auth profiles and displays a usage-window summary next to each provider that has one. - Control UI: Usage displays provider plan and billing cards above OpenClaw's session-derived token and estimated-cost analysis. Anthropic and OpenAI Admin API credentials add provider-reported today, 7-day, and 30-day spend, daily trends, token totals, top models, and cost categories.
- Control UI: the chat composer's context ring popover shows plan usage for subscription providers, including per-window bars (5-hour, weekly, model-scoped) with reset times, the provider plan when known (for example
Max (20x)), and extra-usage credits. Sessions billed through a plan hide per-token dollar estimates; API-billed sessions keepEst. costand the cost-by-type breakdown. Claude Code CLI (claude-cli) setups reuse the same Anthropic subscription usage. - macOS menu bar: a root "Usage" section appears below Context when provider usage snapshots are available. See Menu bar.
openclaw channels list no longer prints provider usage; it directs users to openclaw status or openclaw models list instead.
Anthropic and OpenAI cost history
Subscription quota and API billing are distinct provider surfaces:
- Anthropic subscription/setup credentials continue to show Claude quota windows and optional extra-usage budgets. Set
ANTHROPIC_ADMIN_KEYorANTHROPIC_ADMIN_API_KEYto show organization Usage and Cost API history instead. An Anthropic provider credential starting withsk-ant-adminis detected automatically. - OpenAI ChatGPT/Codex OAuth continues to show plan, quota windows, and credit balance. Set
OPENAI_ADMIN_KEYto show organization cost and completions-usage history instead; optionally setOPENAI_PROJECT_IDto scope it to one project. OpenClaw never sends inference credentials fromOPENAI_API_KEY, provider config, or auth profiles to organization APIs because those keys may belong to custom endpoints.
Admin credentials take precedence because they provide actual organization billing. OpenClaw does not combine these provider-reported totals with its local session estimates; the two sections intentionally answer different questions.
Default usage footer mode
/usage off|tokens|full sets the footer for a session and is remembered for that
session. messages.responseUsage seeds that mode for sessions that have not
chosen one, so the footer can be on by default without typing /usage each time.
Set one mode for every channel, or a per-channel map with a default fallback:
{
"messages": {
"responseUsage": "tokens",
// or: { "default": "off", "discord": "full" }
},
}
Accepted values: "off", "tokens", "full", and the legacy alias "on" (treated as "tokens").
Three distinct session states
A session's responseUsage field has three representable states, each with
different semantics:
| State | Stored value | Effective mode |
|---|---|---|
| Unset / inherit | undefined (absent) | Falls through to messages.responseUsage config default, then off. |
| Explicit off | "off" (stored) | Always off, a non-off config default cannot re-enable the footer. |
| Explicit on | "tokens" or "full" (stored) | That mode, regardless of config default. |
Precedence
Effective mode = session override → channel config entry → default → off.
An explicit /usage off is persisted as the literal value "off" in the
session, not the same as "unset." A non-off messages.responseUsage
default cannot turn the footer back on once the user has explicitly disabled it.
Resetting vs. turning off
/usage offturns the footer off and saves that preference. Even if a non-off default is configured, it cannot override this setting./usage reset(aliases:default,inherit,inherited,clear,unpin) removes the session override. The session then inherits the effective config default (messages.responseUsage). When no default is set, the footer remains off.- A full session reset (
/resetor/new) or a session rollover keeps the explicit usage-mode preference, so the user's display choice persists across session rollovers. Only/usage reset(and its aliases) wipes the override.
Toggle behavior
/usage with no arguments cycles: off → tokens → full → off. The cycle starts from the effective current mode (session override falling through to the config default when unset), so the cycle always aligns with what the user currently sees in the footer.
Config
Without config, the prior behavior remains (footer off until /usage). Use /usage reset to clear a session override and re-inherit the configured default.
Custom /usage full footer
/usage tokens always outputs a plain Usage: X in / Y out line (plus cache and estimated-cost suffixes when available). Only /usage full renders the richer footer described below.
/usage full displays a built-in compact footer with model, reasoning, fast/slow, context window, and cost when those fields are available. No template file is needed for the built-in footer.
messages.usageTemplate is meant only for advanced custom layouts. The value is a JSON file path (supports ~) or an inline object, and it replaces the built-in footer when valid. A file path is watched and reloaded live on change.
{
"messages": {
"usageTemplate": "~/.openclaw/usage-footer.json"
}
}
Missing or empty templates fall back to the built-in footer quietly. Unreadable or invalid configured templates (bad JSON, or a shape with no renderable output pieces) also fall back to the built-in footer and emit an operator warning.
Start custom templates from the built-in shape, then edit the parts you want to change:
{
"schema": "openclaw.usageBar.v1",
"scales": {
"braille": "⠐⡀⡄⡆⡇⣇⣧⣷⣿",
"block": "░▏▎▍▌▋▊▉█",
"shade": "░▒▓█",
"moon": "🌑🌘🌗🌖🌕",
"level": "▁▂▃▄▅▆▇█",
"weather": ["🥶", "☁️", "🌥", "⛅️", "🌤", "☀️"],
"plants": ["", "🍂", "🌱", "☘️", "🍀", "🌿"],
"moons6": ["🌑", "🌚", "🌘", "🌗", "🌖", "🌝"],
},
"aliases": {
"models": {
"claude-opus-4-6": "opus46",
"claude-opus-4-8": "opus48",
"claude-sonnet-4-6": "sonnet46",
"claude-haiku-4-5": "haiku45",
"gpt-5.5": "gpt5.5",
},
"reasoning": {
"off": "🌑",
"minimal": "🌚",
"low": "🌘",
"medium": "🌗",
"high": "🌕",
"xhigh": "🌝",
},
},
"output": {
"sep": "",
"default": [
{ "text": "{model.provider}{identity.emoji|🤖}{model.display_name|alias:models}" },
{ "map": "model.is_fallback", "cases": { "true": "🔄" } },
{ "map": "model.is_override", "cases": { "true": "📌" } },
{ "when": "model.reasoning", "text": "{model.reasoning|alias:reasoning}" },
{ "map": "state.fast_mode", "cases": { "true": "⚡️", "false": "🐌" } },
{
"when": "context.max_tokens",
"text": " | 📚[{context.pct_used|meter:5:braille}]{context.max_tokens|num}",
},
{ "when": "cost.turn_usd", "text": " 💰{cost.turn_usd|fixed:4}" },
],
"surfaces": {
"discord": [
{ "text": "-# -\n" },
{ "text": "-# {model.provider}{identity.emoji|🤖}{model.display_name|alias:models}" },
{ "map": "model.is_fallback", "cases": { "true": "🔄" } },
{ "map": "model.is_override", "cases": { "true": "📌" } },
{ "when": "model.reasoning", "text": "{model.reasoning|alias:reasoning}" },
{ "map": "state.fast_mode", "cases": { "true": "⚡️", "false": "🐌" } },
{
"when": "context.max_tokens",
"text": " | 📚[{context.pct_used|meter:5:braille}]{context.max_tokens|num}",
},
{ "when": "cost.turn_usd", "text": " 💰{cost.turn_usd|fixed:4}" },
],
},
},
}
Shape
{
"schema": "openclaw.usageBar.v1",
"scales": { "<name>": "low-to-high glyphs" }, // string (1 glyph/char) or array
"aliases": { "<table>": { "<value>": "<label>" } },
"output": {
"sep": "", // joins surviving pieces
"default": [/* pieces */], // fallback for any surface
"surfaces": {
"discord": [/* pieces */],
"telegram": [/* pieces */],
},
},
}
Each surface is an ordered list of pieces; the engine renders each, drops empties, and joins survivors with sep. A surface with no entry uses output.default.
Contract Paths
A piece reads values from the per-turn contract by dot-path. Absent values are empty (so a when guard or a |fallback keeps the piece clean).
| Path | Meaning |
|---|---|
surface | channel id (discord/telegram/etc.) |
agentId / chat_type | owning agent id / chat surface kind |
model.id / model.display_name / model.provider | model id / display name / provider id |
model.actual, model.resolved_ref | provider/model ref actually used for the turn |
model.requested | provider/model ref requested (before fallback) |
model.reasoning | effort (off through xhigh) |
model.is_fallback / model.is_override | bool: fallback used / model pinned |
model.override_source / model.auth_mode | override source label / credential mode (oauth, api-key, token, mixed, aws-sdk, unknown) |
state.fast_mode | bool: fast vs slow |
state.compactions | compaction count for the session |
context.max_tokens / context.used_tokens / context.pct_used | window budget / occupied tokens / 0-100 used |
usage.input_tokens / usage.output_tokens / usage.total_tokens | turn aggregate |
usage.cache_read_tokens / usage.cache_write_tokens | cache-read and cache-write tokens for the turn |
usage.has_tokens / usage.has_split_tokens / usage.has_total_only_tokens | token display guards |
usage.cache_hit_pct | cache-read share of total prompt tokens |
usage.last.input_tokens / usage.last.output_tokens / usage.last.cache_hit_pct | final model call only (also has cache_read_tokens, cache_write_tokens, total_tokens) |
cost.turn_usd / cost.available | estimated turn cost / whether a cost table resolved |
timing.duration_ms | wall-clock turn duration |
identity.name / identity.emoji / identity.avatar | agent identity name / emoji / avatar |
session.id | session id |
(Provider rate-limit windows are not in this contract; there is no array-valued path today, so an each piece has nothing to iterate.)
Verbs
Feed a value through verbs in left-to-right order; if a segment isn't a verb, treat it as the fallback.
| Verb | Effect | Example |
|---|---|---|
num | compact count | 272000 -> 272k |
fixed:N | N decimals (0..100, default 2) | 0.0377 |
dur | seconds to duration | 14820 -> 4h07m |
pct | append % | 96 -> 96% |
inv | 100 - x | for used to remaining |
alias:TABLE | lookup in aliases, echo if unlisted | medium -> 🌗 |
meter:W:SCALE | W-cell glyph bar over a 0-100 value | [⣿⣿⠐⠐⠐] (meter:1 = one glyph) |
The only value fixed:N will take is a whole decimal number between 0 and 100. When the precision argument is not valid, that interpolation renders nothing.
For meter:W:SCALE, a complete decimal integer from 1 to 100 is the only accepted input. If the width field is left empty, the fallback is 5 (meter::braille); an invalid width results in an empty interpolation.
Piece forms
{ "text": "📚 {context.max_tokens|num}" }: literal plus interpolation.{ "when": "<path>", "text": "..." }: show only when the path evaluates to truthy.{ "map": "<path>", "cases": { "true": "⚡", "false": "🐌" } }: map a value to a glyph (a_defaultbranch handles anything unmatched).{ "each": "<array-path>", "item": "{label}" }: loop over an array-valued path (no path in the current contract is an array).
Example
{
"schema": "openclaw.usageBar.v1",
"scales": { "braille": "⠐⡀⡄⡆⡇⣇⣧⣷⣿" },
"aliases": { "reasoning": { "medium": "🌗", "high": "🌕" } },
"output": {
"surfaces": {
"discord": [
{ "text": "{model.display_name}" },
{ "when": "model.reasoning", "text": " {model.reasoning|alias:reasoning}" },
{ "map": "state.fast_mode", "cases": { "true": " ⚡", "false": " 🐌" } },
{
"when": "context.max_tokens",
"text": " | 📚 [{context.pct_used|meter:5:braille}]{context.max_tokens|num}",
},
],
},
},
}
produces output like claude-sonnet-4-6 🌗 🐌 | 📚 [⣿⣿⣿⣿⣧]272k.
Providers + credentials
When no usable provider usage auth can be resolved, usage stays hidden. OpenClaw finds enabled provider plugins on its own when they declare contracts.usageProviders and support both resolveUsageAuth and fetchUsageSnapshot; no separate core provider allowlist exists. Discovery stays scoped because the static contract avoids importing every provider plugin. Each plugin is responsible for its own upstream endpoint and response mapping. The shared snapshot keeps plan names, quota windows, balances, spend, and budgets provider-neutral for CLI, app, and Control UI consumers.
- Anthropic (Claude): OAuth tokens stored in auth profiles. When the OAuth token is missing the
user:profilescope, it drops back to aclaude.aiweb session (CLAUDE_AI_SESSION_KEY,CLAUDE_WEB_SESSION_KEY, or asessionKey=cookie inCLAUDE_WEB_COOKIE) if that is configured. Model-scoped limits, along with any enabled extra-usage monthly spend or budgets, are incorporated whenever Anthropic supplies them. An explicit Anthropic Admin API key, or a provider profile auto-detected assk-ant-admin..., instead surfaces 30-day organization cost and Messages API history. - ClawRouter: API key (
CLAWROUTER_API_KEY). A monthly budget window and typed USD budget appear when configured; otherwise, aggregate spend and a request/token/cost summary are shown. - DeepSeek: API key obtained via env, config, or auth store (
DEEPSEEK_API_KEY). Each currency balance reported by the provider is displayed. - GitHub Copilot: OAuth tokens in auth profiles.
- MiniMax: API key or a MiniMax OAuth auth profile. OpenClaw treats
minimax,minimax-cn, andminimax-portalas the same MiniMax quota surface, prefers a stored MiniMax OAuth when one exists, and otherwise falls back toMINIMAX_CODE_PLAN_KEY,MINIMAX_CODING_API_KEY, orMINIMAX_API_KEY. Usage polling takes the Coding Plan host frommodels.providers.minimax-portal.baseUrlormodels.providers.minimax.baseUrlwhen configured, defaulting to the MiniMax CN host otherwise. Since MiniMax's rawusage_percent/usagePercentfields denote remaining quota, OpenClaw inverts them before display; count-based fields take precedence when available.- Window labels rely on provider hours/minutes fields when present, then fall back to the
start_time/end_timespan. - If the coding-plan endpoint returns
model_remains, OpenClaw prefers the chat-model entry, derives the window label from timestamps when explicitwindow_hours/window_minutesfields are missing, and appends the model name to the plan label.
- Window labels rely on provider hours/minutes fields when present, then fall back to the
- OpenAI (Codex/ChatGPT plan): OAuth tokens in auth profiles (with a
ChatGPT-Account-Idheader sent when an account id exists). The ChatGPT plan, resettable Codex windows, and a credit balance appear when reported. Credits stay as provider credits; OpenClaw never labels them as dollars.OPENAI_ADMIN_KEYbrings in 30-day organization cost and completions-usage history when the key has Usage Dashboard access. Inference credentials are never forwarded to organization APIs. - OpenRouter: API key or an OAuth-backed API key (
OPENROUTER_API_KEYor an auth profile). The account credits endpoint and the key quota endpoint are combined, so account balance/spend, key budget, and daily/weekly/monthly usage show up when the credential can reach them. Either endpoint can enrich the snapshot on its own. - Venice: API key via env/config/auth store (
VENICE_API_KEY). USD and DIEM balances, plus DIEM epoch allocation usage, are shown when reported. - Xiaomi MiMo: two distinct usage surfaces. Pay-as-you-go relies on an API key (
XIAOMI_API_KEY); the Token Plan uses a separate key (XIAOMI_TOKEN_PLAN_API_KEY). Neither reports quota windows at this time. - z.ai: API key via env/config/auth store (
ZAI_API_KEYorZ_AI_API_KEY).