---
title: Accounting
---
# Accounting & Metrics Webhook Stream

Token-protected usage accounting and system metrics, exposed as a
**Server-Sent Events (SSE)** stream, a **WebSocket** stream, and a JSON
snapshot.

## Endpoints

| Method | Path | Auth | Description |
|--------|------|------|-------------|
| `GET` | `/accounting/snapshot` | Bearer token | Single JSON snapshot |
| `GET` | `/accounting/stream` | Bearer token | SSE stream of snapshots every `ACCOUNTING_STREAM_INTERVAL` s (default 5) |
| `WS` | `/accounting/ws` | `?token=` or Sec-WebSocket-Protocol | Continuous WebSocket stream of snapshots |

### Authentication

HTTP endpoints require the header:

```
Authorization: Bearer <ACCOUNTING_TOKEN>
```

The WebSocket endpoint accepts the token either as a query parameter
(`?token=<ACCOUNTING_TOKEN>`) or as the first `Sec-WebSocket-Protocol`
value. If `ACCOUNTING_TOKEN` is empty, all accounting endpoints are disabled.

### Example: snapshot

```bash
curl -H "Authorization: Bearer $ACCOUNTING_TOKEN" \
  https://chatagent.emwee.co/accounting/snapshot
```

### Example: SSE stream

```bash
curl -N -H "Authorization: Bearer $ACCOUNTING_TOKEN" \
  https://chatagent.emwee.co/accounting/stream
```

Each SSE event is:

```
event: accounting
data: { ...snapshot json... }
```

### Example: WebSocket stream

```js
const ws = new WebSocket(
  `wss://chatagent.emwee.co/accounting/ws?token=${ACCOUNTING_TOKEN}`
);
ws.onmessage = (e) => {
  const snapshot = JSON.parse(e.data);
  console.log(snapshot.agents_online, snapshot.totals, snapshot.hardware);
};
```

Each WebSocket frame is a JSON object with the exact same schema as a
snapshot (see below), pushed every `ACCOUNTING_STREAM_INTERVAL` seconds.

---

## Snapshot Schema

Every event carries a single JSON object:

```json
{
  "ts": 1785639022.2,
  "agents_online": 3,
  "totals": {
    "messages_received": 120,
    "messages_replied": 115,
    "words_replied": 45200,
    "message_units": 452.0,
    "images_sent": 18
  },
  "per_number": [
    {
      "whatsapp_number": "94707373703",
      "messages_received": 40,
      "messages_replied": 38,
      "words_replied": 15000,
      "message_units": 150.0,
      "images_sent": 6,
      "last_activity": 1785639022.2
    }
  ],
  "tokens": {
    "totals": {
      "input_tokens": 125000,
      "output_tokens": 48000,
      "cache_read_tokens": 30000,
      "cache_write_tokens": 1500,
      "total_tokens": 204500
    },
    "per_model": [
      {
        "provider": "google",
        "model": "gemini-3.8-flash",
        "input_tokens": 80000,
        "output_tokens": 30000,
        "cache_read_tokens": 30000,
        "cache_write_tokens": 1500,
        "total_tokens": 141500,
        "calls": 412
      }
    ],
    "per_number": [
      {
        "whatsapp_number": "94707373703",
        "input_tokens": 45000,
        "output_tokens": 20000,
        "cache_read_tokens": 12000,
        "cache_write_tokens": 600,
        "total_tokens": 77600
      }
    ]
  },
  "hardware": {
    "cpu_percent": 12.5,
    "cpu_count": 8,
    "memory_total_bytes": 17179869184,
    "memory_used_bytes": 6442450944,
    "memory_percent": 37.5,
    "disk_total_bytes": 214748364800,
    "disk_used_bytes": 85899345920,
    "disk_percent": 40.0,
    "uptime_seconds": 86400.5,
    "hostname": "webhook-1",
    "pid": 42
  }
}
```

### Field reference

| Field | Type | Description |
|-------|------|-------------|
| `ts` | number | Unix epoch seconds of the snapshot |
| `agents_online` | integer | Number of currently live agent actors |
| `totals.messages_received` | integer | Total inbound messages across all numbers |
| `totals.messages_replied` | integer | Total replies sent |
| `totals.words_replied` | integer | Total words in all replies |
| `totals.message_units` | number | Billing units, **1 message unit = 100 words** (`words_replied / 100`) |
| `totals.images_sent` | integer | Total product images sent |
| `per_number[].whatsapp_number` | string | The sender's WhatsApp number (wa_id) |
| `per_number[].messages_received` | integer | Inbound messages for that number |
| `per_number[].messages_replied` | integer | Replies for that number |
| `per_number[].words_replied` | integer | Words in replies for that number |
| `per_number[].message_units` | number | `words_replied / 100` for that number |
| `per_number[].images_sent` | integer | Images sent to that number |
| `per_number[].last_activity` | number | Unix seconds of the last recorded activity |
| `tokens.totals.input_tokens` | integer | Total model input (prompt) tokens |
| `tokens.totals.output_tokens` | integer | Total model output (completion) tokens |
| `tokens.totals.cache_read_tokens` | integer | Total cached-prompt tokens read |
| `tokens.totals.cache_write_tokens` | integer | Total cached-prompt tokens written |
| `tokens.totals.total_tokens` | integer | Total tokens (`input + output + cache_read + cache_write`) |
| `tokens.per_model[].provider` | string | LLM provider (google / openai / anthropic) |
| `tokens.per_model[].model` | string | Model id (e.g. `gemini-3.8-flash`) |
| `tokens.per_model[].input_tokens` | integer | Input tokens for that model |
| `tokens.per_model[].output_tokens` | integer | Output tokens for that model |
| `tokens.per_model[].cache_read_tokens` | integer | Cached tokens read for that model |
| `tokens.per_model[].cache_write_tokens` | integer | Cached tokens written for that model |
| `tokens.per_model[].total_tokens` | integer | Total tokens for that model |
| `tokens.per_model[].calls` | integer | Number of LLM calls for that model |
| `tokens.per_number[].whatsapp_number` | string | Sender's WhatsApp number |
| `tokens.per_number[].input_tokens` | integer | Input tokens for that number |
| `tokens.per_number[].output_tokens` | integer | Output tokens for that number |
| `tokens.per_number[].cache_read_tokens` | integer | Cached tokens read for that number |
| `tokens.per_number[].cache_write_tokens` | integer | Cached tokens written for that number |
| `tokens.per_number[].total_tokens` | integer | Total tokens for that number |
| `hardware.cpu_percent` | number | Current CPU utilization (0-100) |
| `hardware.cpu_count` | integer | Logical CPU cores |
| `hardware.memory_total_bytes` | integer | Total physical memory |
| `hardware.memory_used_bytes` | integer | Used physical memory |
| `hardware.memory_percent` | number | Memory usage percent |
| `hardware.disk_total_bytes` | integer | Root filesystem total |
| `hardware.disk_used_bytes` | integer | Root filesystem used |
| `hardware.disk_percent` | number | Root filesystem usage percent |
| `hardware.uptime_seconds` | number | System uptime |
| `hardware.hostname` | string | Node hostname |
| `hardware.pid` | integer | Application process id |

### Billing convention

One reply message is billed as `1` unit per **100 words** (rounded to 2
decimals). Images are counted separately in `images_sent`. Example: a reply of
250 words = `2.5` message units; sending 3 product images adds `3` to
`images_sent`.

---

## Storage

Counters persist in the SQLite database (`CHATAGENT_DB_PATH`) in the
`accounting` table, keyed by `whatsapp_number`. The
[`ConversationStore`](../src/agent/storage.py) exposes:

- `record_inbound(user_id)` — increment received
- `record_reply(user_id, words, images)` — increment replied + words + images
- `accounting_report()` — raw per-number rows

LLM **token usage** is stored in a separate SQLite database
(`CHATAGENT_TOKENS_DB_PATH`, default `./data/tokens.db`) in the
`token_usage` table via [`TokenUsageStore`](../src/accounting/tokens.py):

- `record_usage(number, provider, model, *, input_tokens, output_tokens,
  cache_read_tokens, cache_write_tokens, total_tokens)` — one row per LLM call
- `report()` — aggregated totals + per-model + per-number

Each LLM call inserts a row; `report()` sums cached/input/output/total tokens
per model and per WhatsApp number.
