user.tags to group your users, and usage to report the model response behind each ad request. Both are optional, and neither changes which ads you get.
In Audience → Users you’ll see, by country, tag and model:
- earnings per 1,000 messages
- model cost per 1,000 messages
- Profitability: ad earnings ÷ model cost. 100% is break-even; above 100% is green and below is red. This isn’t a conventional profit margin.
Per-user analytics only count requests that carry a user ID. See Send a user ID on every request.
Group users with tags
user.tags is a flat object of up to 10 keys:
plan. Send plan as free or paid first. It shows what your free users earn, which is the quickest way to see whether ads pay for their model cost.
Report model usage
usage sits at the top level of the ad request. When you request an ad on a new user message, send the previous completed assistant reply’s usage from that same conversation. The new model call hasn’t finished yet. Omit usage on the first turn; save each completed reply’s provider usage for the next turn’s ad request and report it only once. Do not send running totals for the conversation, session or user.
- Input tokens: everything the model read for that one call, including the system prompt, conversation history and other context sent with it. This is not just the latest user message.
- Output tokens: the tokens the model generated in that one reply.
- It never fails a request. Malformed values are ignored.
- Provider keys work as-is.
input_tokens/output_tokensandprompt_tokens/completion_tokensare also accepted, so you can spread a provider’s usage object straight in. - Send
modeleven without token counts. Earnings can still be broken down by model. - Don’t wait for the new model call. Request the ad in parallel, carrying the previous completed reply’s usage. If the conversation ends, that final reply’s usage won’t be reported through a later ad request.
- If you instead request ads after the reply finishes, send that completed reply’s usage then, not again on the next turn.
- One model response per ad request, not per placement. If you use a bid/render integration, send
usageon the bid request, not again on render.
Make the cost figure exact
cost is optional and only makes the cost figure exact. It’s the USD you paid for the model call behind this reply, from 0 to 100:
- When
costis present, the dashboard uses it instead of the list-price estimate. - When it’s absent, cost is estimated from the model maker’s public list price. Gravity doesn’t need your actual costs or discounts.
0means a free call, such as a self-hosted model.- Invalid values are ignored without failing the request.
cost_usdis accepted as an alias.
usage.cost, so usage: { model: res.model, ...res.usage } carries it through with no extra code.
Send both
In these examples,completion is the previous completed model response in this conversation, saved when that reply finished. On the first turn, leave out usage rather than sending zeros.
input_tokens_details.cached_tokens, and Chat Completions returns prompt_tokens_details.cached_tokens. Both are accepted directly, as in the HTTP example.
OpenAI’s input/prompt totals already include cached tokens. Don’t add or subtract them yourself: the list-price estimate uses the published cache-read rate for the cached subset and the normal input rate for the rest. When no cache-read rate is known, cached tokens use the ordinary input rate. Malformed cache counts, including counts greater than total input, are ignored. A valid usage.cost, including 0, still overrides the estimate.
This cache format follows OpenAI semantics, not full Anthropic cache accounting. Anthropic’s input_tokens excludes cache reads and writes, so spreading its raw usage doesn’t provide a full cache-aware estimate. Send the actual cost when you know it.
Send a user ID on every request
Per-user analytics, including everything on this page, only count requests that carry a user ID other than the literal"anonymous". Requests without one still earn, but they drop out of the Users views.
If some of your users can use the app signed out, send a stable anonymous ID for them as user.id (user.userId in gravityContext()), such as an install ID or device ID that you generate once and keep. Don’t use one shared "anonymous" value. Email alone isn’t used as a user key, so a hashed email doesn’t replace user.id.
Next
Request ads
Every field on the ad request.
Payouts & analytics
How revenue and the dashboard work.

