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
curl -H "Authorization: Bearer $ACCOUNTING_TOKEN" \
https://chatagent.emwee.co/accounting/snapshot
Example: SSE stream
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
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:
{
"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 exposes:
record_inbound(user_id)— increment receivedrecord_reply(user_id, words, images)— increment replied + words + imagesaccounting_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:
record_usage(number, provider, model, *, input_tokens, output_tokens, cache_read_tokens, cache_write_tokens, total_tokens)— one row per LLM callreport()— 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.