Measure revenue
Ask your agent to measure gross revenue, refunds, net revenue, transactions and payers in each currency, over time or by any event property, from the amount and currency your events carry, with the per-payer distribution and the top payers, and open the result as a proof page.
On this page
Revenue analytics answers how much money your product took, from how many paying people or accounts, and who they are. It reads the amount and the currency that ordinary product events carry and reports gross revenue, refunds, net revenue, transactions and payers — for the whole window, over time, or split by any event property such as the plan — with the distribution of net revenue per payer and a ranking of your top payers.
It is one kind of the typed product query: revenue. Like
every other kind it is measured once, frozen under an execution key, and read again by result ID.
Your agent runs the measurement through MCP or the CLI. You open the stored result as a read-only
proof page.
Three rules shape everything below:
- Money is exact. An amount is a decimal string with four decimal places, such as
"19.9900". Nothing is rounded, and no figure passes through binary floating point. - Currencies are never added together and never converted. The result has one section per currency. There is no combined total.
- Missing money is never zero. An event without a usable amount is counted and explained, and takes no part in any figure.
Ask your agent
"How much net revenue did we take in September, in each currency, split by plan? Include the three top payers and send me the proof link."
"Compare September's USD revenue with August's. Tell me which events were not counted and why."
| Job | MCP tool or action | CLI command |
|---|---|---|
| Check which revenue shapes and bounds the server accepts | get_product_query_capabilities |
anectico analytics capabilities |
| Measure revenue | query_product_analytics |
anectico analytics query |
| Read a stored revenue result again | get_analytics_result |
anectico analytics result get |
| Page the payers behind a count, or the holder of a top-payer rank | list_analytics_participants |
anectico analytics result participants |
| Prepare a later remeasurement of the same question, and compare the two | get_analytics_remeasurement_source, compare_analytics_results |
anectico analytics remeasurement source, anectico analytics remeasurement compare |
These are MCP read actions. Your agent finds them with list_read_actions and runs them with
execute_read_action. Reading any revenue result needs agents:content:read in addition to
analytics:read, analytics:query and persons:read; the account unit also needs groups:read
(see Permissions). Over MCP the key also needs mcp:read.
What your agent gets back
The result comes back under revenue, with state, closed, amount_scale, accounting,
currencies, other_currencies and truncated. Each currency section carries totals,
per_payer, buckets, breakdown and top_payers. Every amount is an exact decimal string with
four decimal places, and every payers count and top_payers ranking declares a selection_id
that pages the people or accounts behind it. A trimmed example is in
Run it and read the result.
Over MCP, the result carries a link to the proof page in its links, in the entry titled
"Analytics result" (the address is its href). Every tool in the table that reads this stored
result carries the same link, and the comparison tool links both results side by side. The CLI
prints the result and no link.
Open the proof
Open the link from your agent. It opens the stored result in the read-only result viewer, with the project named in the page header. See Proof pages and Result viewer.
The page never measures again. It shows what was stored, when it was measured (in UTC) and when the stored result expires. Its one action is Copy link. To change the question, ask your agent to measure again with a new definition and a new execution key.
You see:
- One section per currency, each labelled with its code and saying that its figures are in that currency only. There is never a combined total; with more than one currency the headline says how many currencies were measured. Each section shows tiles for Net revenue, Gross revenue, Refunds, Transactions, Refund transactions and Payers. A negative net carries its minus sign. Payers are the resolved people, with unresolved identities shown beside them as +N unresolved.
- Net per payer and Net per transaction, labelled Derived: they are net divided by the payers (resolved or not) or by the transactions, on whole ten-thousandths, rounded half away from zero to the currency's usual decimals. A dash means there was nothing to divide by. The exact figure is in the tile's hover text.
- When resolved payers exist, a table of the per-payer distribution: the median, the 90th and 99th percentiles and the highest net, as observed order statistics.
- For a series, a chart per currency with every period also listed in a table beside its exact amounts and its state in words. A period that is not finished or not fully covered is drawn hollow with a dashed edge; a period that could not be measured has no bar and is never drawn as zero.
- For a breakdown, a table per currency with each value, the explicit No value recorded, Explicit null and Invalid value rows when they exist, an Other row and a Truncated note when the limit left values out, and a Total row.
- For top payers, a ranked table per currency of net, gross, refunds and transactions. The page says plainly that opening a row discloses who paid most.
- Data quality, whenever any occurrence was not counted: a small table of how many fell into each reason, with a plain sentence for each, such as "The amount was text, not a number". If more currencies were measured than a result can show, a note says how many were left out and that they are counted, not added.
- For a compared measurement, Compared with another window: a table of Net revenue, Gross revenue, Refunds, Transactions and Payers with one column for this window, one for the comparison window and one for the Difference. A difference is this window minus the comparison window, exactly, and is shown only when both windows are fully measured; otherwise the cell is a dash and the table says why. No percentage is shown. Payers are two separate groups and are never subtracted. A comparison window that is still measuring or lacks coverage is labelled, and one whose coverage is unavailable shows Not measured, never zero.
- A result that is still measuring, has insufficient coverage or is unavailable says so above the
figures, and says in words that the figures are not fully measured; an unavailable result shows
no figures at all. A window with no revenue shows an empty state that explains how to send it.
If the person who opens the link lacks the event-content permission, the page says so and names
agents:content:read.
Send revenue
Revenue is carried by an ordinary product event with two properties:
| Property | Holds | Rules |
|---|---|---|
$revenue |
The amount, as a JSON number. | In major units of the currency (19.99, not 1999). Negative for a refund. At most four decimal places. At most 99,999,999,999.9999 in size. |
$currency |
The currency, as a string. | Exactly three upper-case letters, such as USD or EUR. |
The currency is held to the shape of an ISO 4217 code and is not checked against the registry, so any three upper-case letters are accepted and reported as they are. Anything else is counted but not measured — see Validity. Two cases catch people out:
- A number sent as text (
"19.99") is not a number and is never converted. - An amount with more than four decimal places is not rounded: it is refused as too precise. A sum
computed in floating point such as
0.1 + 0.2is0.30000000000000004and falls into this case. Compute in minor units as whole numbers and divide once (1999 / 100is19.99), or round to the currency's usual number of decimals before you send.
Choose the event or events that mean "money changed hands" — a purchase, a renewal, a refund — and
send the properties on each. The examples send a plan property too, so you can split revenue by
plan later.
JavaScript
captureRevenue is capture() with the two properties set and the amount and currency checked
first. It throws, instead of sending a value revenue analytics would leave out, when the amount is
not a finite number, has more than four decimal places or is too large, or when the currency is not
three upper-case letters. Like capture(), it sends nothing until the visitor has agreed to product
analytics.
import { AnalyticsClient } from '@anectico/sdk/analytics';
const events = new AnalyticsClient({
endpoint: 'https://api.anectico.com',
apiKey: process.env.ANECTICO_CAPTURE_API_KEY!,
});
events.captureRevenue('purchase', { amount: 19.99, currency: 'USD', properties: { plan: 'pro' } });
// A refund is a negative amount.
events.captureRevenue('refund', { amount: -5, currency: 'USD', properties: { plan: 'pro' } });
See the JavaScript SDK reference.
Go
Every SDK other than JavaScript sends the two properties on an ordinary capture. The person comes
from the request context or WithDistinctID, as for any event:
events.Capture(ctx, "purchase", map[string]any{
"$revenue": 19.99,
"$currency": "USD",
"plan": "pro",
}, anectico.WithDistinctID(userID))
Python
events.capture(
'purchase',
{'$revenue': 19.99, '$currency': 'USD', 'plan': 'pro'},
distinct_id=user_id,
)
Send a float or int; a Decimal is not a JSON number, so convert it first.
Android
Write the property names with an escaped dollar sign in Kotlin strings:
Anectico.capture("purchase", mapOf("\$revenue" to 19.99, "\$currency" to "USD", "plan" to "pro"))
iOS
Anectico.capture("purchase", properties: ["$revenue": 19.99, "$currency": "USD", "plan": "pro"])
Flutter
Use raw strings so Dart does not read $ as interpolation:
await Anectico.capture('purchase', properties: {r'$revenue': 19.99, r'$currency': 'USD', 'plan': 'pro'});
React Native
await Anectico.capture('purchase', { $revenue: 19.99, $currency: 'USD', plan: 'pro' });
Use property names you already send
If your events already carry an amount and a currency under other names, do not re-instrument:
name them in the definition with amount_property and currency_property
(for example "amount_property": "order_total"). They are ordinary event properties. The two
names must differ.
Check that revenue arrives
Send one test purchase, then ask your agent to measure revenue for the last hour. The result's
accounting.valid must count your test event. If accounting counts it under a reason such as
amount_not_number or currency_invalid, fix the property that reason names (see
Validity).
Define a measurement
A revenue definition is the common definition (version, window, timezone, optional
environment, optional unit) plus one revenue object:
{
"version": 2,
"timezone": "UTC",
"window": {"absolute": {"start": "2026-09-01T00:00:00Z", "end": "2026-10-01T00:00:00Z"}},
"revenue": {"events": [{"name": "purchase"}]}
}
| Field | Meaning |
|---|---|
revenue.events |
One to ten event selectors, each {"name": "…"} with optional filters of its own. An occurrence that matches two selectors counts once. |
revenue.origins |
Capture origins to read (browser, mobile, platform, server); empty means all. |
revenue.amount_property |
The property that holds the amount. Empty means $revenue. |
revenue.currency_property |
The property that holds the currency. Empty means $currency. |
revenue.currency |
Read the transactions of this one currency (three upper-case letters). Occurrences with no valid currency are still counted for validity. |
revenue.interval |
Empty, day, week (weeks begin on Monday) or month: a series per currency on your timezone's calendar, clipped to the window. |
revenue.breakdown_property |
Split each currency by the value of one event property that the payment's own event carried. The text "2" and the number 2 are different values. |
revenue.breakdown_limit |
How many values to return per currency, 1 to 50; 0 or empty means 10. Only with breakdown_property. |
revenue.top_payers |
How many of each currency's highest payers to rank, 0 to 25; empty or 0 means none. |
unit is {"kind": "person"} (the default) or {"kind": "account", "account_type": "company"} to
count paying accounts by the account an event names. A payment whose event carries no key of
that account type is counted but measured in no account (see
Payers). The account unit refuses person
filters and audiences with QUERY_UNIT_FILTER_UNSUPPORTED.
An interval and a breakdown cannot be combined: choose a time series or a split, not both. The
definition's own breakdown is not accepted either; use revenue.breakdown_property for the
split. A comparison window is accepted on one currency's totals only: see
Compare two windows.
The usual restrictions of the common definition apply: environment, person-property filters and
audiences (cohorts) restrict which people are measured, and event filters narrow the events read.
Examples
Totals for a month, in every currency, for one event:
{
"version": 2,
"timezone": "UTC",
"window": {"absolute": {"start": "2026-09-01T00:00:00Z", "end": "2026-10-01T00:00:00Z"}},
"revenue": {"events": [{"name": "purchase"}]}
}
Monthly revenue for one currency, across a year, on the Europe/Dublin calendar, with the properties named explicitly:
{
"version": 2,
"timezone": "Europe/Dublin",
"window": {"absolute": {"start": "2026-01-01T00:00:00Z", "end": "2027-01-01T00:00:00Z"}},
"revenue": {
"events": [{"name": "purchase"}, {"name": "renewal"}],
"amount_property": "$revenue",
"currency_property": "$currency",
"currency": "EUR",
"interval": "month"
}
}
Revenue by plan, the five biggest plans per currency, with the three top payers:
{
"version": 2,
"timezone": "UTC",
"window": {"absolute": {"start": "2026-09-01T00:00:00Z", "end": "2026-10-01T00:00:00Z"}},
"revenue": {
"events": [{"name": "purchase"}],
"breakdown_property": "plan",
"breakdown_limit": 5,
"top_payers": 3
}
}
Revenue per paying company:
{
"version": 2,
"timezone": "UTC",
"unit": {"kind": "account", "account_type": "company"},
"window": {"absolute": {"start": "2026-09-01T00:00:00Z", "end": "2026-10-01T00:00:00Z"}},
"revenue": {"events": [{"name": "purchase"}], "top_payers": 10}
}
September's USD revenue beside August's:
{
"version": 2,
"timezone": "UTC",
"window": {"absolute": {"start": "2026-09-01T00:00:00Z", "end": "2026-10-01T00:00:00Z"}},
"comparison": {"absolute": {"start": "2026-08-01T00:00:00Z", "end": "2026-09-01T00:00:00Z"}},
"revenue": {"events": [{"name": "purchase"}], "currency": "USD"}
}
Compare two windows
Add a comparison window to measure the same definition over a second window in the same
measurement. Both windows are read at the same moment, under the same identity resolution, so the
two sets of figures describe one observation.
A comparison compares one currency's totals, because amounts in different currencies are never compared. It is accepted only when:
revenue.currencynames the one currency;revenue.interval,revenue.breakdown_propertyandrevenue.top_payersare not set; and- the two windows do not overlap. Windows that only touch, such as two consecutive months, do not overlap.
Anything else is refused with QUERY_REVENUE_INVALID before the query runs.
The result then carries a comparison object beside the usual fields, which keep describing
window:
{
"state": "PRODUCT_REVENUE_STATE_MEASURED",
"currencies": [{"currency": "USD", "totals": {"gross": "1300.0000", "refunds": "50.0000", "net": "1250.0000", "transactions": "6", "refund_transactions": "1", "payers": {"people": "4", "unresolved": "0", "accounts": "0", "selection_id": "…"}}}],
"comparison": {
"state": "PRODUCT_REVENUE_STATE_MEASURED",
"closed": true,
"accounting": {"occurrences": "9", "valid": "9", "amount_missing": "0", "amount_null": "0", "amount_not_number": "0", "amount_out_of_range": "0", "amount_too_precise": "0", "currency_missing": "0", "currency_invalid": "0"},
"account_attribution": null,
"currency": {"currency": "USD", "totals": {"gross": "2000.0000", "refunds": "0.0000", "net": "2000.0000", "transactions": "9", "refund_transactions": "0", "payers": {"people": "7", "unresolved": "0", "accounts": "0", "selection_id": "…"}}, "per_payer": {"payers": "7", "p50": "250.0000", "p90": "500.0000", "p99": "500.0000", "max": "500.0000"}}
}
}
| Field | Meaning |
|---|---|
comparison.state, comparison.closed |
The comparison window's own state. One window can be measured while the other is still open or lacks coverage. |
comparison.accounting |
The comparison window's own validity counts. |
comparison.account_attribution |
For the account unit, the comparison window's attributed and missing transactions. |
comparison.currency |
The currency's totals and per-payer distribution over the comparison window. It is null when that window holds no valid transaction of the currency: in a measured window that is a real zero, not a missing answer. |
manifest.comparison describes the comparison window itself: its range, its coverage and its
population (everyone with a valid transaction in that window). The payers of the comparison window
are a selection of their own, so a person who paid in both windows is listed in each. Page them
with comparison.currency.totals.payers.selection_id, exactly as for the current window.
No difference or percentage is returned: subtract the two exact amounts yourself. The proof page shows the two windows side by side with the exact difference of each amount, and states a difference only when both windows are fully measured.
To be alerted when a measure changes from one period to the next, use a
metric watch on a saved revenue insight;
the watch adds the previous period itself, so the insight needs no comparison of its own.
Money
- Exact decimal strings. Every amount in a result is a string with exactly four decimal
places:
"19.9900","-3.5000","0.0000". It is never a JSON number, so no reader rounds it through floating point. Read it with a decimal type, or as a whole number of ten-thousandths. A result states this inamount_scale(4). - Every sum is exact. Gross, refunds and net are sums of whole ten-thousandths. Ten payments of
0.10make exactly1.0000. - Never converted, never added across currencies. Every figure lives inside one currency's
section, ranked by transactions and then by code. There is no exchange rate and no total of two
currencies. A result holds at most 10 currencies; any more are reported as counts only in
other_currencies(how many currencies, transactions and refund transactions were left out), never as an amount. - A refund is negative at the source and a magnitude in the result.
refundsis the sum of the negative amounts as a non-negative number, andnet = gross - refunds, which can be negative.
What every measure means
The same measures are reported for each currency's totals, for each period of a series, for each value of a breakdown and for the breakdown's other remainder.
| Measure | Definition |
|---|---|
gross |
The sum of the positive amounts. |
refunds |
The sum of the negative amounts, as a non-negative magnitude. |
net |
gross minus refunds, exactly. It may be negative. |
transactions |
The valid occurrences of the set, refunds and zero amounts included. |
refund_transactions |
The valid occurrences with a negative amount. |
payers |
The distinct subjects with at least one positive amount in the set: people, unresolved, accounts and a selection_id. A subject with only refunds or zero amounts is not a payer. See Payers. |
Every uint64 count is a decimal string in JSON.
The server computes no averages. To read revenue per transaction, divide net by
transactions; for revenue per payer, divide net by people plus unresolved (or by accounts).
Divide exact decimals, not floating-point numbers. The proof page does this on whole
ten-thousandths, rounds half away from zero, and labels the result derived.
Over time
With an interval, each currency carries buckets: the calendar periods clipped to the window,
ascending and contiguous. A transaction counts in the period that holds its time, so the periods'
gross, refunds, net and both transaction counts add up to the totals. A payer of two periods
counts in each, so payer counts do not add up. Each period has its own state and closed flag.
A window may hold at most 100 periods.
By property
With a breakdown_property, each currency carries a breakdown:
rowsare the values with the greatest gross, up tobreakdown_limit, ranked by gross, then transactions, then value.missing,nullandinvalidare their own explicit rows, present when they hold a transaction: the payment's event carried no value, an explicit null, or a list, object or non-finite number. A payment is never dropped or folded into another value.otheris the union of the values the limit left out, withvaluessaying how many; its payers are distinct over that union, so they are not the sum of the omitted rows.truncatedistrueexactly whenotheris present.
The rows, the special rows and other add up to the currency's totals. A payer of two values counts
in each, so payer counts do not add up.
Validity: what counts as a transaction
An occurrence is a valid transaction only when its amount is a number with at most four decimal
places within the size limit and its currency is three upper-case letters. Every other matching
occurrence is counted under one reason — the first that applies, in the order below — in the
result's accounting, and takes no part in any figure. occurrences equals valid plus every
reason.
| Reason | The occurrence's |
|---|---|
amount_missing |
amount property is absent. |
amount_null |
amount is an explicit null. |
amount_not_number |
amount is text, true/false, a list or an object. "19.99" is counted here: text is never converted. |
amount_out_of_range |
amount is larger than 99,999,999,999.9999 in size. |
amount_too_precise |
amount has more than four decimal places. It is never rounded. |
currency_missing |
amount is usable but the currency property is absent. |
currency_invalid |
amount is usable but the currency is not exactly three upper-case letters, such as usd, USDT or a number. |
A missing amount is never zero. When you set revenue.currency, an occurrence whose valid currency
is a different one is not selected and appears nowhere; an occurrence with no valid currency is
still accounted for, because it cannot be excluded by a currency it does not have. An event
delivered twice is one occurrence.
For the account unit, account_attribution splits the valid transactions into attributed_events
(the event named an account of the chosen type) and missing_events (it did not). Only the
attributed ones are measured.
Payers: people, accounts and unresolved identities
For the person unit, payers.people counts resolved people and payers.unresolved counts
identities that never resolved to a person. The two are separate counts and are never added
together. An identity that pays anonymously and is later merged into a person is one payer. For
the account unit, payers.accounts counts accounts.
payers.selection_id pages the resolved people (or the accounts) behind a count; unresolved
identities are counted beside them and are not listed. See
Page the people behind a count.
The per-payer distribution and the top payers
per_payer summarizes the net revenue of each payer in a currency: payers (how many),
p50, p90, p99 and max. Money is skewed, so these are observed order statistics, never an
average: the payers' nets are sorted and the one at position floor(level × payers) is returned,
so every figure is an amount one payer actually has, never an interpolation. It covers resolved
people only (or accounts); unresolved identities are not in it. It is absent when the currency has
no resolved payer.
top_payers (when revenue.top_payers is above zero) ranks each currency's highest-paying
resolved people or accounts by net, highest first, ties broken by the subject's key. An unresolved
identity is never ranked. Each entry carries rank, net, gross, refunds and transactions —
money and no identity. The subject of rank N is participant N of top_payers.selection_id,
read through the participants route.
Top payers disclose who paid most. The ranking names nobody, but anyone who can read the result can page the participants of its selection and so learn who your highest payers are. Choose
top_payersonly when that is acceptable for the people who can read the result.
Result states
Missing data is never a measured zero. The whole result, and each period of a series, has a state
and a closed flag (the end of the span against the frozen observation boundary):
| State | Meaning |
|---|---|
PRODUCT_REVENUE_STATE_MEASURED |
The span has ended and platform coverage is sufficient. |
PRODUCT_REVENUE_STATE_IMMATURE |
The span has not ended. The figures observed so far are disclosed and may still grow. |
PRODUCT_REVENUE_STATE_INSUFFICIENT_EVIDENCE |
The span has ended but platform coverage is not sufficient, for example because recent events were still arriving. The observed figures are disclosed and may undercount. |
PRODUCT_REVENUE_STATE_UNAVAILABLE |
Platform coverage is unavailable. Nothing is disclosed: no accounting, no currencies and no selections. |
MEASURED is the only state that means complete. In the middle two the figures are real but not
final; the result's manifest.current.coverage says why. A series reports a state for each
period: a period that ended before the observation boundary can be measured while a later one is
still immature.
Permissions
Amounts, currencies and breakdown values are customer-supplied event content. Reading any
revenue result therefore needs the event-content permission agents:content:read in addition to
analytics:read, analytics:query and persons:read — at execution, and on every later read of
the result, of its people and of its exports. The account unit also needs groups:read. Without
them the request is refused with 403 and a message naming what is missing. See
permissions.
Because who paid is part of the result, anyone who can read a revenue result can page its payers and its top payers.
What is refused
A refused definition returns 400 InvalidArgument with a message that begins with a stable code.
| Code | Refused |
|---|---|
QUERY_REVENUE_INVALID |
The definition's own breakdown; a comparison window without revenue.currency, with an interval, a breakdown_property or top_payers, or overlapping the window; an unknown interval; a blank amount or currency property, or one property named for both; a currency that is not three upper-case letters; a breakdown_limit without a breakdown_property; a breakdown by the amount or the currency property; an interval together with a breakdown_property. |
QUERY_REVENUE_LIMIT |
A breakdown_limit above 50, a top_payers above 25, or an interval that would make more than 100 periods in the window. |
QUERY_SELECTOR_LIMIT |
No event selector, or more than ten. |
QUERY_UNIT_FILTER_UNSUPPORTED |
A person filter or an audience with the account unit. |
A result that would list more than 20,000,000 people across all its payer selections fails with
PRODUCT_RESULT_FAILURE_REASON_EXECUTION_BUDGET instead of being cut short.
Where a revenue result is accepted
| Feature | Revenue |
|---|---|
| Result export | Accepted: the payers of a currency's totals, a period, a value or the top payers export like any other population. The export carries people or accounts and no amounts. |
| Saved insights and dashboard widgets | The recipe is saved and shown as its own result, relative windows included. |
| Scheduled reports | Accepted: the transaction count and each currency's net as figures, and one row per currency (or, for one currency with a series, per period) of exact amounts and counts. Breakdown values stay in the insight. An insight with a comparison window shows the comparison window's transaction count and net beside the current ones when that window is fully measured. |
| Remeasurement | Accepted through MCP, the CLI and REST. |
| Metric watches | Accepted for one measure (net, gross or refunds) of one currency, against a threshold or against the period before: the insight must set currency and no interval, breakdown_property or top_payers. Any other revenue insight is refused with UNSUPPORTED_RESULT_SELECTOR. |
| Live screens | Refused with UNSUPPORTED_RESULT_SELECTOR. |
There is no per-transaction drill-down: analytics result contribution refuses a revenue
reference, so only the roster of payers is available.
Limits
| Limit | Value |
|---|---|
Event selectors (revenue.events) |
1 to 10 |
| Currencies in one result | 10; the rest are counted in other_currencies |
Breakdown values per currency (breakdown_limit) |
1 to 50; 10 when omitted |
Top payers per currency (top_payers) |
0 to 25 |
| Periods in one series | 100 |
| Decimal places of an amount | 4 |
| Size of one amount | 99,999,999,999.9999 |
| People listed across all of a result's payer selections | 20,000,000 |
The measurement limits shared by every kind (measurements in flight, time to run, result lifetime)
are in Limits. anectico analytics capabilities reports the revenue
kind with both units, its intervals, its default revenue_amount_property and
revenue_currency_property, and the max_revenue_currencies, max_revenue_breakdown_rows,
default_revenue_breakdown_rows, max_revenue_buckets, max_revenue_top_payers,
revenue_amount_scale, max_revenue_amount and max_revenue_participant_rows bounds.
Run it and read the result
anectico --project PROJECT_UUID analytics query --file revenue.json --execution-key EXECUTION_UUID
MCP's query_product_analytics takes the same definition, and REST is POST /api/v1/analytics/query
with project_id, execution_key and definition. The response carries the result under
revenue, and only when status is PRODUCT_RESULT_STATUS_READY. Read a result again with
anectico analytics result get RESULT_UUID, MCP's get_analytics_result or
GET /api/v1/analytics/results/{result_id}.
A trimmed result for revenue by plan with top payers. Here the payers were identified as people; one currency is shown:
{
"status": "PRODUCT_RESULT_STATUS_READY",
"revenue": {
"state": "PRODUCT_REVENUE_STATE_MEASURED",
"closed": true,
"amount_scale": 4,
"accounting": {
"occurrences": "16", "valid": "13", "amount_missing": "1", "amount_null": "0",
"amount_not_number": "1", "amount_out_of_range": "0", "amount_too_precise": "0",
"currency_missing": "0", "currency_invalid": "1"
},
"currencies": [
{
"currency": "USD",
"totals": {
"gross": "20.9900", "refunds": "5.0000", "net": "15.9900",
"transactions": "12", "refund_transactions": "1",
"payers": {"people": "2", "unresolved": "0", "accounts": "0", "selection_id": "SELECTION_UUID"}
},
"per_payer": {"payers": "2", "p50": "14.9900", "p90": "14.9900", "p99": "14.9900", "max": "14.9900"},
"buckets": [],
"breakdown": {
"rows": [
{"value": {"string_value": "scale"}, "measures": {"gross": "19.9900", "refunds": "5.0000", "net": "14.9900", "transactions": "2", "refund_transactions": "1", "payers": {"people": "1", "unresolved": "0", "accounts": "0", "selection_id": "SELECTION_UUID"}}},
{"value": {"string_value": "pro"}, "measures": {"gross": "1.0000", "refunds": "0.0000", "net": "1.0000", "transactions": "10", "refund_transactions": "0", "payers": {"people": "1", "unresolved": "0", "accounts": "0", "selection_id": "SELECTION_UUID"}}}
],
"missing": null, "null": null, "invalid": null, "other": null, "truncated": false
},
"top_payers": {
"selection_id": "TOP_PAYERS_SELECTION_UUID",
"entries": [
{"rank": 1, "net": "14.9900", "gross": "19.9900", "refunds": "5.0000", "transactions": "2"},
{"rank": 2, "net": "1.0000", "gross": "1.0000", "refunds": "0.0000", "transactions": "10"}
]
}
}
],
"other_currencies": null,
"truncated": false
}
}
In this example three of the sixteen occurrences were not counted: one had no amount, one sent its
amount as text, and one used a lower-case currency. Net revenue per transaction is
15.9900 / 12, and net per payer 15.9900 / 2; the figures above are the exact inputs.
Page the people behind a count
Every payers and every top_payers declares a selection_id. Page the resolved people (or
accounts) behind it with the same call as any other kind:
anectico --project PROJECT_UUID analytics result participants RESULT_UUID \
--selection-id SELECTION_UUID --limit 50
MCP's list_analytics_participants and GET /api/v1/analytics/results/{result_id}/participants
take the same selection_id, limit and cursor; continue with next_cursor and keep the result,
selection and limit unchanged. A selection lists resolved people only: unresolved identities
are counted in unresolved and are not listed. Each selection can also be exported.
To learn who holds rank N in top_payers, read the participants of
top_payers.selection_id: entry N is participant ordinal N. A ranking holds at most 25 entries,
so the first page of 25 or more covers it.
What revenue does not do
- No currency conversion. Amounts are never converted and never added across currencies. There is no exchange rate and no combined total.
- No subscription metrics. There is no monthly recurring revenue, annual recurring revenue, churn, expansion, contraction or cohort revenue retention. A payment is a transaction, not a subscription.
- No lifetime value model. There is no predicted or modelled customer value. Net per payer is what the window's payers paid, divided exactly.
- Alerting covers one currency's total. A metric watch alerts on the net, gross or refunds of an insight that names one currency, compared each period with a threshold — including a period with no transactions, which is an amount of 0 (see When revenue stops) — or with the period before it (see When revenue changes). It does not alert on a time series, a breakdown value or a top payer, and it never combines currencies.
- A comparison is two windows of one currency's totals. A time series, a breakdown and a top payers ranking are measured for one window only, and no percentage change is computed for you.
- No live screens. A live screen cannot show a revenue
result: it is refused with
UNSUPPORTED_RESULT_SELECTOR. - Only what your events report. Revenue describes what the selected events carried. An event with no usable amount or currency is counted and explained, not guessed. Comparing the revenue of two groups is an observation, not evidence that one caused the other.