Configuration
Config file format, agent overrides, capture settings, storage, and merge rules.
TokenJam reads its config from ~/.config/tj/config.toml (created by tj init). All settings can be overridden via the CLI, the REST API, or the web UI. The file is the source of truth and is regenerated when you save.
Example
# ~/.config/tj/config.toml (generated by tj init)
[defaults.budget]
daily_usd = 10.00
[agents.my-email-agent]
description = "Personal email management agent"
[agents.my-email-agent.budget]
daily_usd = 5.00
session_usd = 1.00
[[agents.my-email-agent.sensitive_actions]]
name = "send_email"
severity = "critical"
[agents.my-email-agent.drift]
enabled = true
baseline_sessions = 10
token_threshold = 2.0
[capture]
prompts = true # default
tool_inputs = true # default
completions = false # default
tool_outputs = false # default
[storage]
path = "~/.config/tj/telemetry.duckdb"
retention_days = 90
Merge rules
Budget limits merge per-field: each agent inherits [defaults.budget] unless overridden. Setting daily_usd on one agent doesn’t reset session_usd to nothing.
Plan tier and framing
TokenJam knows two distinct budget concepts. Don’t conflate them.
[defaults.budget]/[agents.<id>.budget]— per-agent alert thresholds (daily_usd,session_usd) checked on every span.[budget.<provider>]— per-provider config: your declared plan tier, and an optional cycle ceiling read by thebudget-projectionanalyzer and by abudget_capproxy policy.
tj init prompts for your plan tier and writes it to [budget.<provider>] plan. You can set it non-interactively with tj init --plan <tier>, or edit it by hand:
[budget.anthropic]
plan = "max_5x" # api | pro | max_5x | max_20x | plus | team | enterprise | local
# usd = 200 # optional cycle ceiling: budget-projection and budget_cap policies
[budget.openai]
plan = "plus" # api | plus | team | enterprise
The plan tier drives how dollar figures are rendered everywhere — the CLI, the REST API, and the web UI all read one shared framing rule rather than deciding on their own:
| Plan tier | How figures are shown |
|---|---|
API (api) | Dollar costs verbatim. |
Subscription (pro, max_5x, max_20x, plus, team, enterprise) | Token-share framing — ”% of cycle” and “implied API value”, never a spend claim. |
Local (local) | Tokens only. No dollars. |
| Unknown | Dollars suppressed with a hint to re-run tj init --reconfigure. |
This is why a Max-plan user sees quota-share language while an API user sees dollars: the plan tier, not the command, decides the framing.
Capture settings
Two of the four flags default on. prompts and tool_inputs are recorded unless you turn them off, because several analyzers go dark without them: cache-recommend and trim never fire, reuse never reaches its prompt-prefix mode, script and verbosity fall back to tool names alone, and tj optimize --validate has nothing to replay. Most of those are already skipped for a window an interactive coding agent dominates. If that is all you run, what turning the flags off actually costs you is tj optimize --validate, which has no prompt to replay without prompts, and the exact form of Shipped’s repeat-edit figure, which falls back to counting on path alone without tool_inputs. completions and tool_outputs default off, because completion text is the largest payload to store and no analyzer’s detection depends on it. One deliverable does: Reuse renders its cluster skeleton from the planning call’s completion text, so without completions the clusters and numbers still appear and the skeleton is replaced by a hint.
Everything captured stays in the local DuckDB on your machine.
| Flag | Default | What it records |
|---|---|---|
prompts | on | The user/system prompt sent to the model. |
tool_inputs | on | The arguments of each tool call. |
completions | off | The model’s response text. |
tool_outputs | off | The raw return value of each tool call. |
When capture.* is false, only metadata (tokens, cost, latency, span structure) is stored.
Custom model rate overrides
TokenJam ships a packaged model rate table (tokenjam/pricing/models.toml, USD per million tokens) that prices most public models with zero configuration. These are model token rates only — the numbers used to turn token counts into a cost estimate. They are not a product plan.
Inspect the resolved table any time:
tj pricing list # every (provider, model) with input/output/cache rates
tj pricing list --model claude # filter by substring
tj pricing list --json
The source column tells you whether each row came from the packaged table (packaged) or a local override (override).
The packaged table can’t know your situation — negotiated rates, open-weight models hosted elsewhere, or a self-hosted endpoint. Override rates locally, no PR and no package edit needed. There are two places to put an override, and two ways to key it.
Where overrides live (later wins):
- A
[pricing]section in your main config (tj.toml/.tj/config.toml/~/.config/tj/config.toml) — the project-local home. - A standalone file at
~/.config/tj/pricing.toml, or any path in theTJ_PRICING_FILEenvironment variable.
The project-local [pricing] section wins over the standalone file, which wins over the packaged table.
Two ways to key a rate:
Provider-keyed corrects a rate for a specific provider and model:
[pricing.anthropic]
"claude-haiku-4-5" = { input_per_mtok = 0.80, output_per_mtok = 4.00, cache_read_per_mtok = 0.08, cache_write_per_mtok = 1.00 }
Model-keyed pins a rate to a bare model name regardless of which provider TokenJam inferred. Reach for this when a model’s provider resolves to unknown (open-weight or unattributed traffic). It lives under a reserved models section:
[pricing.models]
"llama-3.3-70b" = { input_per_mtok = 0.59, output_per_mtok = 0.79 }
"claude-haiku-4-5" = { input_per_mtok = 0.50, output_per_mtok = 2.50 } # your negotiated rate
In the standalone ~/.config/tj/pricing.toml, the reserved section is simply [models] and provider-keyed entries stay at the top level:
[models]
"llama-3.3-70b" = { input_per_mtok = 0.59, output_per_mtok = 0.79 }
[anthropic]
"claude-haiku-4-5" = { input_per_mtok = 0.50, output_per_mtok = 2.50 }
Only input_per_mtok and output_per_mtok are required; the cache rates default to 0.0. Dated variants (claude-haiku-4-5-20251001) are priced off the base-name entry automatically.
Lookup order (first match wins): model-keyed override, then provider-keyed override, then the packaged table, then a $0.50 input / $2.00 output flat default (logged once) when nothing matches.
The rate table is cached for the process lifetime, so restart the daemon (tj stop then tj serve) to pick up an edit.
Local API auth
tj serve’s read routes are unauthenticated by default, on the assumption that 127.0.0.1 is your own machine. To require a key on them:
[api]
host = "127.0.0.1"
port = 7391
[api.auth]
enabled = true
api_key = "a-secret-you-choose"
With enabled = true, every read route wants Authorization: Bearer <api_key>. Lens keeps working, because tj serve injects the key into the dashboard HTML it serves and the page sends it on every call.
That bootstrap is why this setting is not a way to expose tj serve beyond your own machine. The dashboard route is not itself behind the key, so anyone who can reach the port can load the page and read the key out of it. It raises the bar for a script pointed straight at /api/v1/*, and it is not a boundary. To reach the dashboard from elsewhere, tunnel to it or front it with something that authenticates, and leave the listener on loopback.
The ingest routes are separate and always take the ingest secret, and the write endpoints Lens calls carry a per-process token of their own that this setting never affects.
Storage
DuckDB is the only datastore. The file lives at [storage].path and is opened read-write by tj serve and read-only by the MCP server and CLI. retention_days controls automatic pruning of old spans.
Discovery order
tj looks for config in this order:
--config <path>CLI flagTJ_CONFIGenvironment variable./tj.config.toml(current directory)~/.config/tj/config.toml(default, written bytj init)
The first match wins.
Verification
Run tj doctor to check that your config is well-formed and matches what the daemon is using:
tj doctor
doctor reports missing fields, invalid types, conflicting overrides, and whether the daemon picked up your most recent edits.