# `PhoenixKitAgent.Spend`
[🔗](https://github.com/BeamLabEU/phoenix_kit_agent/blob/v0.1.0/lib/phoenix_kit_agent/spend.ex#L1)

What users spent on the assistant (spec §7.3), counted from the AI request
journal (`phoenix_kit_ai_requests`) rather than from the module's own
messages: a row of the user (`user_uuid`) whose `metadata.source` is
`"agent"` — every model call of the module passes both, so a call that
describes an attachment is counted too. The journal is not pruned by age, so
the month never loses rows. The journal keeps a cost in dollars (`cost`, a
`numeric` that can be finer than a micro-dollar); the sums are in
micro-dollars ($1 = 1_000_000): each window is added up in dollars and only
then rounded, half up, to a whole micro-dollar
(`PhoenixKitAgent.Config.cost_to_micro/1`) — two rows of $0.0000004 are
`1`, not `0`. Only endpoints that report a cost add to it.

Every row with a cost counts, whatever its `status`: a call that failed or
timed out after the provider billed it is money spent all the same. Today
`phoenix_kit_ai` writes a cost only on its `"success"` rows (a failed call is
logged with `cost` `NULL`, which adds nothing), so the sums equal a
success-only count. For the same reason the queries do not filter on
`status = 'success'` and cannot use core's partial index
`phoenix_kit_ai_requests_user_spend_idx`; they go by the journal's
`user_uuid` index and the month's window.

The windows are calendar days and months in the **site time zone**
(Settings → `time_zone`; `PhoenixKit.Utils.TimeZone`), each from its local
midnight to the next one — so a day that has 23 or 25 hours is counted right.

Turns are counted from the module's own messages (`turns_today/2`): a turn
is one `user` message with a `turn_ref`.

# `spent`

```elixir
@type spent() :: %{
  day_micro_usd: non_neg_integer(),
  month_micro_usd: non_neg_integer()
}
```

# `window`

```elixir
@type window() :: {DateTime.t(), DateTime.t()}
```

Half-open: from the first instant, up to but not including the second.

# `for_users`

```elixir
@spec for_users([String.t()], DateTime.t()) :: %{required(String.t()) =&gt; spent()}
```

What each of `user_uuids` spent today and this month (the windows of `now`):
`%{user_uuid => %{day_micro_usd:, month_micro_usd:}}`, every user in the
answer, zero when there is nothing.

# `format_usd`

```elixir
@spec format_usd(integer()) :: String.t()
```

Micro-dollars as dollars with at least two decimals: `1_500_000` is `"1.50"`.

# `period_keys`

```elixir
@spec period_keys(DateTime.t()) :: %{day: String.t(), month: String.t()}
```

The local day and month that contain `now`, as keys: `%{day: "2026-10-19",
month: "2026-10"}`.

# `turns_today`

```elixir
@spec turns_today([String.t()], DateTime.t()) :: %{
  required(String.t()) =&gt; non_neg_integer()
}
```

How many turns each of `user_uuids` started today (the local day of `now`),
in any of their chats: `%{user_uuid => count}`, every user in the answer.

# `users_this_month`

```elixir
@spec users_this_month(DateTime.t()) :: [String.t()]
```

Everyone who used the assistant this month (the local month of `now`): a
request in the journal or a turn. User uuids; a journal row whose user was
deleted has none and is left out, and so is a call the AI guard refused
(`metadata.refused`): it was never made.

# `windows`

```elixir
@spec windows(DateTime.t()) :: %{day: window(), month: window()}
```

The local day and month that contain `now`, as UTC windows.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
