# Cost Observability

The worker instruments per-client cost telemetry using Cloudflare Analytics Engine.
All events are written to the `fourai_cost_metrics` dataset.

> **Billing independence:** Cost tracking is separate from client billing. Billing uses credits consumed (messages, automations) only. Cost observability tracks all platform events (AI queries, container activity, R2 ops, cron triggers, WebSocket sessions, action completions, action steps) for internal cost analysis and infrastructure optimization. Billing events are a subset of cost tracking events. See [VISION.md](/llms/platform/reference/vision/index.md#billing-vs-cost-tracking).

## Setup

The `ANALYTICS` binding must exist in your Cloudflare account before deploying.
It is declared in `wrangler.jsonc` and maps to the `fourai_cost_metrics` dataset.

Customer identity is read from the `AGENT_TENANT` env var (e.g. `arua`).
If unset it defaults to `default`.

## Event Schema

### `ai-query`

Emitted for each AI response received over the WebSocket proxy.

| Field     | Type   | Description                           |
| --------- | ------ | ------------------------------------- |
| `blob1`   | string | Event type: `"ai-query"`              |
| `blob2`   | string | Model name (e.g. `claude-3-5-sonnet`) |
| `blob3`   | string | Provider (e.g. `anthropic`)           |
| `blob4`   | string | Customer identifier                   |
| `blob5`   | string | Action type                           |
| `blob6`   | string | Request ID                            |
| `blob7`   | string | Session ID (empty if not set)         |
| `double1` | number | Input tokens                          |
| `double2` | number | Output tokens                         |
| `double3` | number | Estimated cost in USD                 |
| `double4` | number | `1` if cached, `0` otherwise          |

### `container-active`

Emitted on every `ensureGateway()` call (≈ every request) with a 10-second
index bucket to allow deduplication.

| Field     | Type   | Description                              |
| --------- | ------ | ---------------------------------------- |
| `blob1`   | string | Event type: `"container-active"`         |
| `blob2`   | string | Customer identifier                      |
| `blob3`   | string | Instance type (e.g. `standard-1`)        |
| `blob4`   | string | Action: `start` \| `heartbeat` \| `stop` |
| `blob5`   | string | Gateway process ID                       |
| `double1` | number | Always `1` (count)                       |

### `r2-operation`

Emitted for R2 backup/restore operations.

| Field     | Type   | Description                                                           |
| --------- | ------ | --------------------------------------------------------------------- |
| `blob1`   | string | Event type: `"r2-operation"`                                          |
| `blob2`   | string | Customer identifier                                                   |
| `blob3`   | string | Operation type: `backup` \| `restore` \| `sync` \| `head` \| `delete` |
| `blob4`   | string | R2 pricing class: `ClassA` \| `ClassB`                                |
| `double1` | number | Bytes transferred                                                     |
| `double2` | number | Duration in milliseconds                                              |

### `cron-trigger`

Emitted on every scheduled cron invocation.

| Field     | Type   | Description                               |
| --------- | ------ | ----------------------------------------- |
| `blob1`   | string | Event type: `"cron-trigger"`              |
| `blob2`   | string | Customer identifier                       |
| `blob3`   | string | Trigger reason                            |
| `double1` | number | Always `1` (count)                        |
| `double2` | number | `1` if container was woken, `0` otherwise |

### `ws-session`

Emitted when a WebSocket session closes.

| Field     | Type   | Description                 |
| --------- | ------ | --------------------------- |
| `blob1`   | string | Event type: `"ws-session"`  |
| `blob2`   | string | Customer identifier         |
| `blob3`   | string | Session ID                  |
| `double1` | number | Session duration in seconds |
| `double2` | number | Total messages exchanged    |

### `action-completed`

Emitted when an agent action completes (message or automation).

| Field     | Type   | Description                            |
| --------- | ------ | -------------------------------------- |
| `blob1`   | string | Event type: `"action-completed"`       |
| `blob2`   | string | Customer identifier                    |
| `blob3`   | string | Instance ID (empty if not set)         |
| `blob4`   | string | Action type: `message` \| `automation` |
| `blob5`   | string | Channel type (e.g. `slack`, `api`)     |
| `blob6`   | string | Agent identifier                       |
| `blob7`   | string | Status: `success` \| `failure`         |
| `blob8`   | string | Failure reason (empty on success)      |
| `double1` | number | Input tokens                           |
| `double2` | number | Output tokens                          |
| `double3` | number | Cost in USD                            |
| `double4` | number | Duration in milliseconds               |

### `action-step` (v0.3.0)

Emitted for each individual step within an action (AI query, tool call, exec call, etc.).

| Field     | Type   | Description                                  |
| --------- | ------ | -------------------------------------------- |
| `blob1`   | string | Event type: `"action-step"`                  |
| `blob2`   | string | Customer identifier                          |
| `blob3`   | string | Action ID                                    |
| `blob4`   | string | Step type: `ai_query` \| `tool_call` \| etc. |
| `blob5`   | string | Tool name (empty if not a tool step)         |
| `blob6`   | string | Outcome: `completed` \| `error` \| `blocked` |
| `double1` | number | Duration in milliseconds                     |
| `double2` | number | AI cost in USD (0 for non-AI steps)          |
| `double3` | number | Infrastructure cost in USD                   |
| `double4` | number | Credits consumed (1 if completed step)       |

## SQL Queries

Analytics Engine uses a SQL-like syntax. Replace `fourai_cost_metrics` with
your actual dataset name if it differs.

### Per-customer AI spend (last 24 hours)

```sql
SELECT
  blob4  AS customer,
  blob2  AS model,
  SUM(double1) AS tokens_in,
  SUM(double2) AS tokens_out,
  SUM(double3) AS cost_usd
FROM fourai_cost_metrics
WHERE blob1 = 'ai-query'
  AND timestamp > NOW() - INTERVAL 1 DAY
GROUP BY customer, model
ORDER BY cost_usd DESC
```

### Container active time per customer

Each heartbeat covers a 10-second bucket. Multiply count by 10 for seconds.

```sql
SELECT
  blob2 AS customer,
  blob3 AS instance_type,
  COUNT(*) * 10 AS estimated_active_seconds
FROM fourai_cost_metrics
WHERE blob1 = 'container-active'
  AND blob4 = 'heartbeat'
  AND timestamp > NOW() - INTERVAL 1 DAY
GROUP BY customer, instance_type
```

### Container start events

```sql
SELECT
  blob2 AS customer,
  COUNT(*) AS cold_starts
FROM fourai_cost_metrics
WHERE blob1 = 'container-active'
  AND blob4 = 'start'
  AND timestamp > NOW() - INTERVAL 7 DAY
GROUP BY customer
ORDER BY cold_starts DESC
```

### R2 bandwidth per customer

```sql
SELECT
  blob2 AS customer,
  blob3 AS op_type,
  SUM(double1) AS bytes_transferred,
  AVG(double2) AS avg_duration_ms,
  COUNT(*) AS op_count
FROM fourai_cost_metrics
WHERE blob1 = 'r2-operation'
  AND timestamp > NOW() - INTERVAL 7 DAY
GROUP BY customer, op_type
```

### Cron wake rate (container sleep efficiency)

A high `woke_rate` means the container is frequently sleeping between crons —
consider lowering `SANDBOX_SLEEP_AFTER`.

```sql
SELECT
  blob2 AS customer,
  COUNT(*) AS total_crons,
  SUM(double2) AS woke_count,
  SUM(double2) / COUNT(*) AS woke_rate
FROM fourai_cost_metrics
WHERE blob1 = 'cron-trigger'
  AND timestamp > NOW() - INTERVAL 7 DAY
GROUP BY customer
```

### WebSocket session duration distribution

```sql
SELECT
  blob2 AS customer,
  COUNT(*) AS sessions,
  AVG(double1) AS avg_duration_s,
  MAX(double1) AS max_duration_s,
  AVG(double2) AS avg_messages
FROM fourai_cost_metrics
WHERE blob1 = 'ws-session'
  AND timestamp > NOW() - INTERVAL 7 DAY
GROUP BY customer
```

## Cost Estimation

The `GET /api/admin/costs` endpoint returns the pricing constants used by
`estimateContainerCost()` and an example GraphQL query for the AI Gateway
dashboard.

### Container pricing (Cloudflare, as of Jul 2026)

| Resource | Rate                    | Billing model                  |
| -------- | ----------------------- | ------------------------------ |
| CPU      | $0.00002 / vCPU-second  | Active usage × 0.2 utilisation |
| Memory   | $0.0000025 / GiB-second | Provisioned (always billed)    |
| Disk     | $0.00000007 / GB-second | Provisioned (always billed)    |

### Instance types

| Type         | vCPU | Memory   | Disk  |
| ------------ | ---- | -------- | ----- |
| `lite`       | 1/16 | 0.25 GiB | 2 GB  |
| `basic`      | 1/4  | 1 GiB    | 4 GB  |
| `standard-1` | 1/2  | 4 GiB    | 8 GB  |
| `standard-2` | 1    | 6 GiB    | 12 GB |
| `standard-3` | 2    | 8 GiB    | 16 GB |
| `standard-4` | 4    | 12 GiB   | 20 GB |

The default instance is `standard-1`. Memory and disk are billed continuously
regardless of CPU activity, so the sleep setting (`SANDBOX_SLEEP_AFTER`)
does **not** reduce memory/disk costs.
