# 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

```toml
# ~/.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 the `budget-projection` analyzer and by a `budget_cap` [proxy policy](/docs/proxy#policies).

`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:

```toml
[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](/docs/optimize-more#when-an-analyzer-does-not-run) 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:

```bash
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):

1. A `[pricing]` section in your main config (`tj.toml` / `.tj/config.toml` / `~/.config/tj/config.toml`) — the project-local home.
2. A standalone file at `~/.config/tj/pricing.toml`, or any path in the `TJ_PRICING_FILE` environment 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:

```toml
[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:

```toml
[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:

```toml
[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:

```toml
[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:

1. `--config <path>` CLI flag
2. `TJ_CONFIG` environment variable
3. `./tj.config.toml` (current directory)
4. `~/.config/tj/config.toml` (default, written by `tj init`)

The first match wins.

## Verification

Run `tj doctor` to check that your config is well-formed and matches what the daemon is using:

```bash
tj doctor
```

`doctor` reports missing fields, invalid types, conflicting overrides, and whether the daemon picked up your most recent edits.