*Social Media Manual — [index](../MANUAL.md)*

# 10. Usage and billing

Every outbound HTTP request the connector makes to a platform is recorded in a ledger, one
row per call. The ledger is how DataHandler bills pro rata for platforms that charge per
request, and how an owner can see what was done with their accounts and by whom.

## What is recorded

| field | meaning |
|---|---|
| `owner_member_id` | the **billable party**: the owner of the account the call was made for |
| `actor_member_id` | who caused it: the tool caller, the member who requested the publication, the member who started the connect |
| `account_id`, `platform` | which account; a credential connect happens before the account row exists, so that one call has no `account_id` |
| `origin` | `tool:<name>`, `job:publish`, `job:reconcile`, `web:callback` |
| `principal`, `request_id` | the tool call, when there was one |
| `publication_id` | when the call served a publication |
| `operation` | `"METHOD host/path"` with ids collapsed to `{id}` and the query string dropped; no token ever lands in it |
| `http_status`, `duration_ms`, `bytes_out`, `bytes_in`, `error` | the call itself |
| `billable` | true on paid platforms (X by default); Meta calls are recorded but not billable. Follows the host actually called: fetching a media file from our own store during an X publish is not billable |
| `charge`, `price` | what the platform lists for that call at the time it was made (see the rate card below); `price` is `0` when the platform rejected the call, both are empty when the call is **unpriced** (the card has no entry for it) |

The ledger never breaks a platform call: if the insert fails, the call still goes through and
the failure is logged server-side.

## The rate card

X bills pay-per-use, per request, from prepaid credit; there is no subscription. The
connector prices every X call it makes at X's published list price (USD, October 2026):

| charge | what | price |
|---|---|---|
| `post_create` | a post, plain text or text + media | 0.015 |
| `post_create_url` | a post whose text contains a link (`http(s)://` or `www.`) | 0.200 |
| `post_delete` | deleting a post | 0.010 |
| `post_read` | reading one of the account's own posts back | 0.001 |
| `user_read` | the "who am I" lookup during a connect | 0.001 |
| `media_upload` | each media upload request (an image is three: INIT, APPEND, FINALIZE) | 0.005 |
| `token` | token refresh | free |

The link price is the one to warn about: a post with a URL costs over 13x a plain post.
Facebook and Instagram have no card; their calls are recorded, not billable, not priced.
The operator can change any price without a deploy (`social.pricing` in the server config);
`rate_card` in the `social_get_usage` reply is what is in force now.

Prices are estimates at list price. The invoice is the platform's; what the ledger
establishes is each owner's **share** of it, so a drift in X's actual rates does not change
who pays what fraction as long as the ratios hold.

## Who is billed for what

An editor's publish under an authorization is attributed to the editor and billed to the
account owner. Schedules bill the owner too. Reading a post back, editing it, deleting it, a
token refresh before a call, a reconciliation attempt: all are calls, all are recorded. Drafts,
previews, media uploads, invitations and authorizations make no platform calls and cost
nothing.

## `social_get_usage`

Owner only; editors and viewers see nothing (`ACCESS_DENIED` is not raised, the result is
simply scoped to accounts the caller owns, so for a non-owner it is empty).

Arguments: `from`, `to` (ISO-8601 date or datetime, UTC; default the first of the current
month to now), optional `account_id`. `PERIOD_INVALID` for a bad date or `from` after `to`.

Reply:

| field | meaning |
|---|---|
| `period` | the resolved `from` / `to` |
| `billable_platforms` | which platforms are billable on this server |
| `rate_card` | per billable platform, the USD list price of each charge kind in force now |
| `totals` | `calls`, `billable_calls`, `failed_calls`, `bytes_out`, `priced_calls`, `unpriced_calls`, `estimated_cost_usd` |
| `by_account` | per account: handle, platform, calls, billable, failed, `estimated_cost_usd` |
| `by_operation` | per collapsed operation, with `estimated_cost_usd` |
| `by_actor` | per member who caused calls, with `estimated_cost_usd` |
| `by_origin` | tool / job / web |
| `by_charge` | per billable platform and charge kind: `unit_price_usd`, `calls`, `estimated_cost_usd` (`charge` null = unpriced calls) |

When a user asks "what did this cost" give them `totals.estimated_cost_usd` for the period,
`by_charge` to show what drove it (links are the usual answer), `by_account` if they own
several, and name the actors if the account is shared. Say it is an estimate at list price:
the bill is the platform's, and the operator splits it by each owner's share. If
`unpriced_calls` is not 0, say so: those calls are counted but could not be priced.

## The operator report

The operator runs a monthly report that prints one CSV line per owner and platform (default:
the previous calendar month) for invoicing: the owner's estimated cost, their share of the
platform's total, and, when the operator enters the amount the platform actually billed, the
owner's slice of that invoice. It reads the same ledger, so what the owner sees in
`social_get_usage` for a past month is what they are billed on.
