Stats
GET /stats/dashboard
Section titled “GET /stats/dashboard”Get dashboard statistics
Returns aggregate stats with avg/min/max for time saved, ads removed, cost, processing time, episode length, and token usage. Optionally filtered by podcast. Episode-scoped fields (totalEpisodesProcessed, episodesWithTimeSaved, and the episode length and time-saved figures) count each surviving episode once; run-scoped fields (ads, audio cues, cost, tokens, processing time, totalRuns) count every completed processing run, including reprocesses.
Parameters
Section titled “Parameters”podcast_slug(query, string): Filter by podcast slug
Responses
Section titled “Responses”| Status | Description | Body |
|---|---|---|
| 200 | Dashboard statistics | object |
200 response body
Section titled “200 response body”totalEpisodesProcessed(integer)totalRuns(integer)episodesWithTimeSaved(integer)avgTimeSavedSeconds(number)avgAdsRemoved(number)avgCostPerEpisode(number)avgProcessingTimeSeconds(number)avgEpisodeLengthSeconds(number)totalInputTokens(integer)totalOutputTokens(integer)totalLlmCost(number)avgInputTokens(integer)avgOutputTokens(integer)
GET /stats/by-day
Section titled “GET /stats/by-day”Get processing counts by day of week
Parameters
Section titled “Parameters”podcast_slug(query, string): Filter by podcast slug
Responses
Section titled “Responses”| Status | Description | Body |
|---|---|---|
| 200 | Per-day stats | object |
200 response body
Section titled “200 response body”days(array of object)days[].day(string)days[].dayIndex(integer)days[].count(integer)days[].avgAds(number)
GET /stats/by-podcast
Section titled “GET /stats/by-podcast”Get per-podcast aggregate stats
episodeCount counts distinct surviving episodes with at least one completed run; runCount counts every completed processing run, including reprocesses.
Responses
Section titled “Responses”| Status | Description | Body |
|---|---|---|
| 200 | Per-podcast stats ordered by total ads | object |
200 response body
Section titled “200 response body”podcasts(array of object)podcasts[].podcastSlug(string)podcasts[].podcastTitle(string)podcasts[].episodeCount(integer)podcasts[].runCount(integer)podcasts[].totalAds(integer)podcasts[].avgAds(number)podcasts[].avgEpisodeLengthSeconds(number)podcasts[].avgTimeSavedSeconds(number)podcasts[].totalCost(number)podcasts[].totalInputTokens(integer)podcasts[].totalOutputTokens(integer)podcasts[].avgTokensPerEpisode(integer)
GET /stats/reviewer
Section titled “GET /stats/reviewer”Get ad reviewer aggregate stats
Aggregate counts and averages from the ad_reviewer_log table. Returns zero counts when the reviewer has not run.
Parameters
Section titled “Parameters”podcast_slug(query, string)episode_id(query, string)
Responses
Section titled “Responses”| Status | Description | Body |
|---|---|---|
| 200 | Reviewer stats | object |
200 response body
Section titled “200 response body”totalReviews(integer)verdictCounts(object)verdictCounts.confirmed(integer)verdictCounts.adjust(integer)verdictCounts.reject(integer)verdictCounts.resurrect(integer)verdictCounts.inconclusive(integer)verdictCounts.failure(integer)pass1AdjustmentCount(integer)pass2AdjustmentCount(integer)avgBoundaryShiftSeconds(number)resurrectionCount(integer)failureCount(integer)byMode(object): Verdicts per addressing mode the ad was detected under. Rows logged before 2.100.12 carry no mode and are excluded.byMode.timestamps(ReviewerModeStats)byMode.segment_ids(ReviewerModeStats)
GET /stats/addressing
Section titled “GET /stats/addressing”Get per-addressing-mode compliance stats
Aggregate counts from the addressing_log table, grouped by effective addressing mode (timestamps / segment_ids). A ‘random’-configured run records under whichever mode was drawn for that pass. Always returns both modes, zeroed when a mode has no rows.
Parameters
Section titled “Parameters”podcast_slug(query, string)
Responses
Section titled “Responses”| Status | Description | Body |
|---|---|---|
| 200 | Addressing-mode compliance stats | object |
200 response body
Section titled “200 response body”modes(object)modes.timestamps(AddressingModeStats)modes.segment_ids(AddressingModeStats)byMode(object): Outcomes per addressing mode the marker was detected under. Rates are fractions of corrections that carry a mode; avgSnapDistance covers stored markers from the last 90 days. Null when a mode has no data.byMode.timestamps(AddressingModeOutcomes)byMode.segment_ids(AddressingModeOutcomes)
GET /stats/model-usage
Section titled “GET /stats/model-usage”Get paginated spend by provider and model
Paginated, sortable, filterable breakdown of the llm_call_usage ledger grouped by (provider, model). A model id shared by two providers is reported as two separate rows. Spend includes billed failures, not only successes; unknownCostCount counts billable rows whose cost could not be resolved so an unpriced model’s spend stays visibly incomplete instead of reading as $0.
Parameters
Section titled “Parameters”page(query, integer)limit(query, integer)sortBy(query, string, one ofprovider,model,calls,distinctEpisodes,inputTokens,outputTokens,knownCostUsd,unknownCostCount,cacheHitRate): Unrecognized values fall back to knownCostUsd.sortDir(query, string, one ofasc,desc)from(query, string): Start of the window. Accepts a bare YYYY-MM-DD date or an ISO 8601 timestamp, and selects whole UTC days: the value is truncated to the start of its UTC day, so that whole day is included. An unparseable value is a 400.to(query, string): End of the window, inclusive of the whole UTC day it names, down to its final fractional second. Same accepted formats as from, and the same 400 on an unparseable value.podcastSlug(query, string)provider(query, string)model(query, string)
Responses
Section titled “Responses”| Status | Description | Body |
|---|---|---|
| 200 | Paginated provider/model spend | object |
| 400 | A from or to value could not be parsed as a date or timestamp. from is checked first, so only the first offending parameter is named. | Error |
200 response body
Section titled “200 response body”items(array of object)items[].provider(string)items[].model(string)items[].calls(integer)items[].distinctEpisodes(integer)items[].inputTokens(integer)items[].outputTokens(integer)items[].cacheReadTokens(integer): Summed from the ledger; cached tokens are counted inside inputTokens.items[].cacheWriteTokens(integer): Summed from the ledger; cached tokens are counted inside inputTokens.items[].knownCostUsd(string): Decimal string; sum of rows with a resolved cost.items[].unknownCostCount(integer): Billable rows whose cost could not be resolved.total(integer)totalPages(integer)page(integer)limit(integer)
GET /stats/systemone
Section titled “GET /stats/systemone”Get native System One call statistics
Logical calls include failures before HTTP dispatch. Requests count actual reserved dispatches; logical latency and dispatch latency are measured separately in milliseconds. Known cost is a decimal USD sum; unknown usage and cost counts include failed and cancelled requests. Only allowlisted diagnostic evidence is included; prompts, transcripts, and credentials are excluded. Statistics are retained for the ledger lifetime. Deleting a feed removes its associated diagnostics.
Parameters
Section titled “Parameters”from(query, string): Start of the selected UTC date range; accepts a date or ISO 8601 timestamp.to(query, string): Inclusive final UTC day; accepts a date or ISO 8601 timestamp.podcastSlug(query, string): Filter by exact podcast scope.provider(query, string): Filter by provider key.model(query, string): Filter by configured model ID.
Responses
Section titled “Responses”| Status | Description | Body |
|---|---|---|
| 200 | Native logical-call and request aggregates | SystemOneStats |
| 400 | A from or to value could not be parsed as a date or timestamp. from is checked first, so only the first offending parameter is named. | Error |
GET /stats/llm
Section titled “GET /stats/llm”Get LLM call statistics
Counts ledger rows from non-System One providers as calls; requests count actual dispatches. Latencies are in milliseconds, known cost is a decimal USD sum, and unknown usage and cost counts include failed and cancelled requests.
Parameters
Section titled “Parameters”from(query, string): Start of the selected UTC date range; accepts a date or ISO 8601 timestamp.to(query, string): Inclusive final UTC day; accepts a date or ISO 8601 timestamp.podcastSlug(query, string): Filter by exact podcast scope.provider(query, string): Filter by provider key.model(query, string): Filter by configured model ID.
Responses
Section titled “Responses”| Status | Description | Body |
|---|---|---|
| 200 | LLM call and request aggregates | LlmStats |
| 400 | A from or to value could not be parsed as a date or timestamp. from is checked first, so only the first offending parameter is named. | Error |
GET /stats/cleanup
Section titled “GET /stats/cleanup”Get Pattern Cleanup accounting and actions
Attributed usage, checks, and proposals use the cleanup run start date. Separate unattributedUsage uses request time for older unlinked ledger rows and is absent under a podcastSlug filter. Only podcast-scoped patterns have exact feed allocation; global, network, and historical unknown scopes are unallocated. Provider/model filters require exact attempt attribution. Checked counts are attempts, including failed or stale checks. Historical reported reviews are separate from exact checks; historical per-run spend is unavailable. Superseded proposals and compact records of deleted patterns preserve action totals. Applied means approved and not undone, including actions on subsequently deleted patterns; it does not mean the pattern remains active.
Parameters
Section titled “Parameters”from(query, string): Start of the selected UTC date range; accepts a date or ISO 8601 timestamp.to(query, string): Inclusive final UTC day; accepts a date or ISO 8601 timestamp.podcastSlug(query, string): Filter by exact podcast scope.provider(query, string): Filter by provider key.model(query, string): Filter by configured model ID.
Responses
Section titled “Responses”| Status | Description | Body |
|---|---|---|
| 200 | Exact cleanup cohorts and historical coverage | CleanupStats |
| 400 | A from or to value could not be parsed as a date or timestamp. from is checked first, so only the first offending parameter is named. | Error |
GET /stats/episode-costs
Section titled “GET /stats/episode-costs”Get paginated per-episode LLM cost
Paginated, sortable, filterable per-episode breakdown of the llm_call_usage ledger. The provider/model filter never reorders which run counts as latest: latestRunCostUsd and lastActivityAt always reflect the episode’s actual latest run, honoring the from/to window if set. cumulativeCostUsd sums only the runs that match every filter, including provider/model, so the two cost figures diverge once an episode has been reprocessed.
Parameters
Section titled “Parameters”page(query, integer)limit(query, integer)sortBy(query, string, one ofpodcastSlug,podcastTitle,episodeTitle,runCount,latestRunCostUsd,cumulativeCostUsd,lastActivityAt): Unrecognized values fall back to lastActivityAt.sortDir(query, string, one ofasc,desc)from(query, string): Start of the window. Accepts a bare YYYY-MM-DD date or an ISO 8601 timestamp, and selects whole UTC days: the value is truncated to the start of its UTC day, so that whole day is included. An unparseable value is a 400.to(query, string): End of the window, inclusive of the whole UTC day it names, down to its final fractional second. Same accepted formats as from, and the same 400 on an unparseable value.podcastSlug(query, string)provider(query, string)model(query, string)
Responses
Section titled “Responses”| Status | Description | Body |
|---|---|---|
| 200 | Paginated per-episode cost | object |
| 400 | A from or to value could not be parsed as a date or timestamp. from is checked first, so only the first offending parameter is named. | Error |
200 response body
Section titled “200 response body”items(array of object)items[].podcastSlug(string)items[].podcastTitle(string)items[].episodeId(string)items[].episodeTitle(string)items[].modelsUsed(array of string)items[].runCount(integer)items[].latestRunCostUsd(string): Decimal string; cost of the episode’s actual latest run. Not narrowed by the provider/model filter.items[].cumulativeCostUsd(string): Decimal string; cost summed across every run matching all filters, including provider/model.items[].lastActivityAt(string)total(integer)totalPages(integer)page(integer)limit(integer)
GET /stats/episode-costs/runs
Section titled “GET /stats/episode-costs/runs”Get per-run cost breakdown for one episode
Per-run, per-phase cost rows for one episode, used by the expandable Episode costs row in Stats. Same run shape as the episode detail page’s processingRuns.
Parameters
Section titled “Parameters”slug(query, string, required)episodeId(query, string, required)
Responses
Section titled “Responses”| Status | Description | Body |
|---|---|---|
| 200 | Runs for the episode, oldest first | object |
| 400 | slug and episodeId are required | Error |
| 404 | Episode not found | Error |
200 response body
Section titled “200 response body”runs(array of EpisodeProcessingRun)
GET /stats/spend/attempts
Section titled “GET /stats/spend/attempts”List the ledger attempts behind a run’s or episode’s spend
The individual llm_call_usage rows a spend total was summed from, which the Stats Incomplete chip links to so an unknown cost can be traced to the attempt carrying no price. Scope it with runId, or with slug plus episodeId to cover every run of one episode. Applies the same billable predicate as the totals, so the listed rows are exactly the contributing ones. Capped at 200 rows; truncated says when more exist, and the totals still cover them.
Parameters
Section titled “Parameters”runId(query, string): The run whose attempts to list. Required unless slug and episodeId are given.slug(query, string): Feed slug, paired with episodeId to scope to one episode.episodeId(query, string): Episode id, paired with slug.provider(query, string): Limit the listing to one provider key.
Responses
Section titled “Responses”| Status | Description | Body |
|---|---|---|
| 200 | Contributing attempts for the run | object |
| 400 | Neither runId nor slug plus episodeId was given | |
| 404 | Feed not found |
200 response body
Section titled “200 response body”runId(string or null)episodeId(string or null)provider(string or null)total(integer, required): Number of attempts returned.unknownCostCount(integer, required): Attempts with no recorded price.knownCostUsd(string, required): Decimal string sum of the priced attempts.truncated(boolean, required)attempts(array of object, required)attempts[].attemptId(string, required)attempts[].phase(string, required)attempts[].invokingPass(integer or null)attempts[].provider(string, required)attempts[].credentialSlot(string): Account slot; a null column reads as primary.attempts[].model(string, required)attempts[].returnedModel(string or null)attempts[].status(string, one ofsuccess,failure,inconclusive, required): Ledger state. Inconclusive means the reviewer abstained and the original marker was retained.attempts[].inputTokens(integer or null)attempts[].outputTokens(integer or null)attempts[].cacheReadTokens(integer or null)attempts[].cacheWriteTokens(integer or null)attempts[].costUsd(string or null): Decimal string, or null when the call was never priced.attempts[].costSource(string or null)attempts[].createdAt(string (date-time))attempts[].finalizedAt(string or null (date-time))
GET /stats/ledger-filter-options
Section titled “GET /stats/ledger-filter-options”List the provider and model values present in the usage ledger
Distinct providers and models the llm_call_usage ledger holds, for populating the Stats page filter dropdowns. It honors the same from/to/podcastSlug scope as the two list endpoints, so the options describe the data actually in view, but it is never paginated: a provider or model that only appears on a later page of a list request is still selectable. Rows with a null provider or model are dropped. providers is sorted and deduplicated; pairs preserves which models belong to which provider, so a model id served by two providers stays distinguishable.
Parameters
Section titled “Parameters”from(query, string): Same whole-UTC-day semantics as the list endpoints. Bare YYYY-MM-DD or ISO 8601; unparseable is a 400.to(query, string): Inclusive of the whole UTC day it names. Unparseable is a 400.podcastSlug(query, string)
Responses
Section titled “Responses”| Status | Description | Body |
|---|---|---|
| 200 | Filter options for the ledger lists | object |
| 400 | A from or to value could not be parsed as a date or timestamp. from is checked first, so only the first offending parameter is named. | Error |
200 response body
Section titled “200 response body”providers(array of string, required)pairs(array of object, required): Every distinct provider and model combination, ordered by provider then model.pairs[].provider(string, required)pairs[].model(string, required)