# Cost per action

Read what each agent action costs all-in by tenant, agent and day, alert on $ per 1,000 actions, route cheap steps to open-weight models, and time each stage.

[Cost and usage](/docs/monitor/cost-usage) prices model calls and priced tools. This page is about a different unit: the **agent action**, which is what buyers compare across platforms. Every action is counted per tenant, agent and day with what it cost to serve.

## What is counted

The ledger meters four kinds of event:

| Kind | What |
| --- | --- |
| `model` | A model call. A response-cache hit is a model action that cost nothing at the model |
| `tool` | A tool call |
| `approval` | An approval raised |
| `retrieval` | A knowledge retrieval |

An **action** is one model step and the tool call it chose. So `$ per 1k actions` divides by model steps, never by every metered event: a step with a tool call and an approval is one action, not three. The total also reports `events`, every metered row.

Each action carries three costs:

| Cost | Where it comes from |
| --- | --- |
| Model $ | The usage ledger's own price of the call: tokens times your rate card or the catalogue's list price, with cached and cache-write tokens at their own rates. Taken as the call is recorded, so the two never disagree |
| Vendor $ | A tool's configured per-call price, plus `TICKETIQ_ACTION_COST_APPROVAL_USD` per approval and `TICKETIQ_ACTION_COST_RETRIEVAL_USD` per retrieval (both default `0`) |
| Infra $ | `TICKETIQ_ACTION_INFRA_USD_PER_1K` per 1,000 actions (default `0.35`) |

> [!NOTE]
> **The infra share is a configured constant, not a measurement.** The default of $0.35 per 1,000 actions is a modelled figure: one small node and its disk at a reference workload of 100,000 actions a month. Liyagent does not read your actual hosting bill. Set the value to your own infrastructure cost divided by your action volume.

Infra is charged once per action, on its model step. It is not charged on the tool calls, approvals and retrievals around it, nor on a second model call that answers the same step (an open-weight try that falls back). A response-cache hit is still an action and still carries infra.

Counts are kept in memory and written every `TICKETIQ_ACTION_COST_FLUSH_S` seconds (default `5`), and before any read, so an action never waits on the database. `TICKETIQ_ACTION_COST_ENABLED=false` turns the ledger off.

## Read the numbers

Open **AI Gateway** › **Cost and usage**. The **Cost per action** panel shows actions in the last 7 days and the all-in $ per 1,000, by tenant.

| Question | Endpoint |
| --- | --- |
| All-in totals and $ per 1,000 actions over the last `days` (1 to 400, default 7) | `GET /api/usage/actions?days=7` |
| Grouped by tenant, agent, day or kind | `GET /api/usage/actions?group_by=agent` |
| One agent | `GET /api/usage/actions?agent=<key>` |

Each total has `actions`, `model_usd`, `vendor_usd`, `infra_usd`, `usd`, `usd_per_1k_actions`, `model_usd_per_action`, tokens, `cached_input_share`, `cache_hits`, `open_weight` (actions answered open-weight) and `events`. The response also carries the target and the alert settings.

Reading needs `governance.read`. A caller confined to a tenant reads only that tenant's rows. Each read is audited as a spend read.

## Alerts on $ per 1,000 actions

After each flush, a tenant whose day has at least `TICKETIQ_ACTION_COST_ALERT_MIN_ACTIONS` actions (default `100`) and costs more than its threshold raises one governance alert per tenant per day, audited as `action_cost.alert`.

The default threshold is `TICKETIQ_ACTION_COST_ALERT_USD_PER_1K` (default `1.00`). Change it, or set per-tenant thresholds, with:

```bash
curl -X PUT https://<host>/api/usage/actions/alerts -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"usd_per_1k": 1.0, "tenants": {"acme": 2.5}}'
```

This needs `governance.write` and is audited as `usage.actions.alerts.save`.

## The open-weight route

The model is most of what an action costs, and many steps do not need a frontier model. The open-weight route sends eligible steps to an open-weight model first, and to the agent's own binding when it fails or is unsure.

- **Where it runs.** A self-hosted OpenAI-compatible server on your own network (vLLM, llama.cpp, TGI, Ollama), or a SageMaker real-time endpoint, set up as an `openai` provider.
- **Eligible steps.** A step a caller declared (classification, extraction, routing), or a short one: no tools, no images, no history, and an output cap of at most `TICKETIQ_OPENWEIGHT_SHORT_ANSWER_TOKENS` (default `400`).
- **Governed the same way.** The step goes through the normal governed path: kill switch, Cedar `call_model` on that provider, guardrails, budgets, metering and transcript. A provider the agent may not use is refused, and the step goes to the frontier model.
- **Fallback.** The answer is used unless it failed, came back empty, was cut at the cap, or reads as unsure: a JSON `confidence` under `TICKETIQ_OPENWEIGHT_MIN_CONFIDENCE` (default `0.6`) or a refusal. A guardrail block is not retried.
- **Cache.** Classification, extraction and routing steps are answered from the response cache for `TICKETIQ_OPENWEIGHT_CACHE_TTL_S` (default `3600`; `0` turns it off). The cache is keyed by the prompt and the caller's authorization.

Nothing is routed until a provider exists: set `TICKETIQ_OPENWEIGHT_PROVIDER` to an existing provider's name, or `TICKETIQ_OPENWEIGHT_BASE_URL` to seed a provider called `openweight` at startup. `TICKETIQ_OPENWEIGHT_ROUTING=false` turns the route off.

| Task | Endpoint | Permission |
| --- | --- | --- |
| Configuration, and what this process has routed: eligible steps, the share answered open-weight, fallbacks by reason, cache hits, prompt-cache share | `GET /api/openweight` | `governance.read` |
| Change `enabled`, `provider`, `min_confidence`, `short_answer_tokens`, `cache_ttl_s`, or opt agents out with `agents: {"<key>": false}` | `PUT /api/openweight` | `governance.write` |

The statistics are per process and reset when it restarts.

## Per-stage latency

Every HTTP request and every agent step is timed per stage: `auth`, `policy`, `guardrails`, `retrieval`, `model`, `tool`, `approval`, `audit` and `metering`. Stages are exclusive: a stage nested in another is charged to itself and taken out of the one around it.

**Governance overhead** is the total minus `model` and `tool`. It includes unnamed framework, serialisation and database time, so it is never flattered by leaving time out.

| Where | What |
| --- | --- |
| `GET /api/perf/stages` (`governance.read`) | p50, p95 and p99 of each stage, the total and the governance overhead, for this process's recent requests and agent steps, beside the targets: governance overhead p95 50 ms, API p95 250 ms at 200 requests a second |
| `GET /metrics` | `ticketiq_stage_duration_seconds` and `ticketiq_governance_overhead_seconds` histograms |
| `Server-Timing` header | Sent to an authenticated caller that sends `X-Liya-Timing: 1`. Never sent to an anonymous caller |
| Traces | One span per request or step, with a child per stage, only when trace export is configured, sampled at `TICKETIQ_PERF_SPAN_SAMPLE_RATE` (see [Telemetry](/docs/operate/telemetry)) |

`TICKETIQ_PERF_STAGES_ENABLED=false` turns the timing off. Nothing is written to the database for it.

```bash
curl -s -D - -o /dev/null -H "Authorization: Bearer $TOKEN" -H "X-Liya-Timing: 1" \
  https://<host>/api/agents | grep -i server-timing
```

## Next steps

- [Cost and usage](/docs/monitor/cost-usage)
- [Budgets](/docs/ai-gateway/budgets)
