TEASEDocs
ProductsClubConcepts

Dashboards & metrics

The buckets, funnels, and access rules behind every number the Club dashboards show you — and why an empty block is never a guess.

Club's dashboards (Money, Live pulse, the Fansly account board, and any custom block you build) all draw from the same small set of underlying ideas. This page explains them once so the how-to pages can stay short.

The money leaderboard's four buckets

Every paying fan on the Money dashboard's leaderboard lands in exactly one of four buckets. In the panel they show up as:

BucketWhat it meansPanel label
A resolved sourceThe fan came in through a link/domain you track.the source's own name
OrganicPaid, but arrived with no source signal at all (direct visit, search, word of mouth).Organic (OF)
PendingMoney or a subscription exists, but nothing is assigned yet — a link can still fix this.Needs a source
CheckingA brand-new paying fan whose event-driven source check hasn't finished.Checking source

The fourth bucket — checking — is easy to miss if you only know the leaderboard from older docs: it holds fans whose money is already visible but whose tracking-link check is still in flight. It is not a queue you work through by hand; it resolves itself as the check completes, and its row only appears on screen once it actually holds a fan.

“balanced: true” is not the real integrity check

The leaderboard response carries a balanced flag (buckets sum to the account total), but that sum is true by construction — any leftover money is deliberately swept into organic, so it can never fail to balance, even if a fan were double-counted. The real double-count guard is a separate pair of fields: how many bucket memberships were counted vs. how many distinct fans that represents. When they differ, a fan landed in more than one bucket at once — that's the bug balanced can't see.

Evidence level: how sure is a row?

Every leaderboard row (real source, organic, pending) carries an evidence level built from its own money, not carried over from anywhere else:

  • hard — a click or a link/campaign we can point to.
  • llm — a source guessed from reading a fan's DM history.
  • mixed — the row folded both a hard and an llm-attributed fan together.
  • none — no evidence at all (the sentinel rows).

This matters most when rows get rolled up (a creator- or tenant-wide view sums several pages together): the evidence label is recomputed from the summed money every time, never copied forward from the first page. A row that started "hard" on page one but absorbed a DM-guessed fan from page two is relabeled "mixed" — otherwise the label would quietly overstate how solid the row's evidence is.

The fan-entry funnel and cohort trend

Every leaderboard row (and the account-wide summary) carries the same seven-counter funnel: new, old, ftl (free-trial-link entries), paid (paid-link entries), paid_after (entries or returns that actually paid), ftl_paid (trial entries that converted), and entered (everything, including unknown-type entries). Its "new" edge tolerates a one-hour window of vendor clock skew, so a fan entering in the last hour of a window isn't wrongly excluded.

The cohort trend — the day-by-day "new fans' revenue" line behind the ARPNU chart — is built from that same funnel scan and the same payment scan the revenue trend already loaded. It costs no extra database query: it's aligned index-for-index with the revenue trend series, so the two can be drawn on the same axis without a second fetch.

Empty blocks explain themselves

A dashboard block that has nothing to show never guesses why. The backend returns one of exactly four machine codes:

  • no_data_in_window — collection is working, this window just had no events.
  • not_connected — no successful collection has ever happened for this account.
  • source_not_collected — the platform doesn't send this data; it will never resolve on its own.
  • maturing — the numbers exist but the platform hasn't closed out the window yet.

The panel's Why? button on an empty block reads this code straight from the server. See Diagnose why a dashboard block is empty for what to actually do about each one.

The metrics registry and per-key access

Every number that can go into a custom block, or into the batch GET /dashboard/metrics read, comes from one shared registry: a passport per metric key (money.net, fans.active_paid, fans.mrr_cents, traffic.*, tg.channel_subs, Fansly's fyp.*, and more) declaring its unit, whether it has a history series, and which access rule protects it. A metric key can never enter the registry without a matching access rule — that's checked at import time, not discovered later as a live bug.

Access is checked per key, not per request

A team member who can only see traffic data doesn't get a blanket 403 on a batch that also asks for money keys. Each key in the batch is checked on its own: their traffic.* keys come back with real numbers, and their money.* keys come back {"error": "forbidden"} — same response, same 200. The panel shows those individual tiles as "no access" instead of failing the whole card. See Permission gating surprises for what this looks like in practice.

Custom dashboard blocks

Beyond the built-in tiles, an owner can assemble their own blocks — a number, a sparkline, a chart, a formula, or a set of bars/donut/funnel slices — by picking metrics straight out of the registry. See Build a custom dashboard block for the actual flow; conceptually, a custom block is just a small stored recipe (which metrics, which chart type, an optional formula) that reads the same registry and obeys the same per-key access rules as everything else.

Background: the walk reserve

One attribution signal never shows up as a button anywhere: the walk reserve. When a fan's first touch is a trial code, and campaign membership records make exactly one source unambiguous for that code, a background writer fills in that fan's source automatically — but only where it is certain, and never by overwriting a stronger click/link source or another writer's own canonical mapping. It's a quiet enrichment step behind the organic/pending split, not a task you work.

What's next

On this page