What the public
says about the story.
Echo enriches Polari story clusters with public discourse signal — controversy scores, dominant themes, counter-narratives, and sentiment from Reddit, Bluesky, and X/Twitter. It tells you what editorial coverage missed.
Echo sits above the pipeline
The four pipeline layers process editorial content — articles, their entities, their clusters, their relationships. Echo is different. It listens to what happens in public discourse after a story is established, and attaches that signal back to the cluster.
This means Echo enrichment is
always cluster-scoped. You need a cluster_id from Layer 2 to query Echo. The enrichment you get
back tells you how the public received that story — which entities they introduced that editorial coverage
didn't mention, whether they're pushing back on the dominant narrative, and how much controversy the story
generated across platforms.
Polari story clusters move through lifecycle states as editorial coverage evolves: breaking →
developing → mature → concluded. Echo introduces a fifth state:
contested.
A story becomes contested when editorial coverage has gone quiet — the story is
concluded from a journalism standpoint — but public discourse is still active and generating
high controversy (≥ 0.6) or a high-confidence counter-narrative (≥ 0.7), with enrichment freshness within
48 hours. The story isn't over. The public just hasn't caught up, or disagrees with how it was covered.
contested surfaces on Layer 2's story list —
GET /v1/stories — computed live per-story on every read when Echo enrichment is present.
It is also computed and persisted whenever POST /v1/story/{id}/analyze runs. The single-story
read, GET /v1/story/{id}, does not currently compute or return lifecycle_state
at all. It is not a field on the Echo endpoints documented below either way.
Currently considers Reddit and Bluesky signal only. Twitter/X
enrichment does not yet feed into the contested calculation — a story can have high
Twitter controversy or a confident Twitter counter-narrative and still not trigger the state. This is
expected to be resolved as Twitter's dataset matures; the Echo endpoints themselves (search,
trending, story) already return Twitter data in full regardless.
Endpoints Professional+
Base URL:
https://echo.api.polariapi.com · All endpoints require Authorization: Bearer {key}
except /health. Starter tier keys receive 403 tier_restricted.
The primary Echo endpoint. Returns the full public discourse enrichment for a story cluster — sentiment, controversy, dominant themes, entities, and counter-narrative analysis, broken down by platform.
| Field | Type | Description |
|---|---|---|
| platform | string | reddit, bluesky, or twitter. One object per platform with
enrichment data. |
| thread_urls | string[] | Source thread/post URLs the enrichment was derived from. |
| fetched_at / updated_at | string (ISO 8601) | When the underlying content was fetched, and when this row was last written. Usually equal;
updated_at advances on re-enrichment even if fetched_at logic doesn't
change.
|
| sentiment_score | float | Aggregate sentiment across analyzed posts/comments. –1 (negative) to +1 (positive). |
| controversy_score | float | 0–1. Scores ≥ 0.6 contribute to contested lifecycle state (see callout above). |
| dominant_themes | string[] | LLM-synthesized themes from public discourse — often diverge from editorial framing. |
| editorial_entities | string[] | The story's own editorial entity set — the baseline novel_entities is computed
against. Not "entities in both." |
| public_entities | string[] | All named entities extracted from public discourse for this platform, including ones that overlap with editorial coverage. |
| novel_entities | string[] | public_entities minus editorial_entities — entities the public is
discussing that editorial coverage never mentioned. The primary counter-narrative signal. |
| counter_narrative_detected | bool | Whether a counter-narrative was identified. |
| counter_narrative_summary | string | null | Natural language description of the counter-narrative, synthesized by LLM from public posts. Null if not detected. |
| counter_narrative_confidence | float | 0–1. Confidence ≥ 0.7 contributes to contested state. |
Batch retrieval of Echo enrichments for up to 50 story clusters in a single request.
| Parameter | Type | Description |
|---|---|---|
| ids required | string | Comma-separated cluster IDs. Max 50. |
| platform | string | Optional single-platform filter: reddit, bluesky, or twitter.
|
Response is an object keyed by story ID. Clusters
with no Echo enrichment are omitted rather than returning null entries — check the missing
array in the response to see which requested IDs had no data.
Stories with the highest public controversy or active counter-narratives right now — ordered by controversy score descending. Useful for surfacing what the editorial layer hasn't caught up with yet.
| Parameter | Type | Description |
|---|---|---|
| min_controversy | float | Minimum controversy score threshold. Default 0.6. |
| counter_narrative_only | bool | If true, only returns stories where a counter-narrative was detected. Default
false.
|
| platform | string | Filter by source platform: reddit, bluesky, or twitter. Omit
for all platforms. |
| limit | integer | Max results. Default 20, max 50. |
Modular search across all Echo enrichments — combine entity, topic, counter-narrative flag, controversy
threshold, and platform in any combination. Use this when you don't already have a specific
cluster_id and want to find stories matching a pattern, rather than looking up one you already
know.
| Parameter | Type | Description |
|---|---|---|
| entity | string | Entity name or partial name. Matching is substring (ILIKE), not exact — deliberately
loose to catch entity-extraction noise (handle-like strings, split variants). |
| entity_scope | string | editorial | public | novel | any. Default
any. novel is the counter-narrative signal specifically — entities the
public is discussing that never appeared in editorial coverage.
|
| topic | string | Substring match against dominant_themes. |
| counter_narrative | bool | Filter to true/false counter-narrative detection. |
| min_controversy | float | Minimum controversy score, 0–1. |
| platform | string | Comma-separated list, e.g. reddit,bluesky. Omit for all platforms. An unrecognized or
currently-inactive platform returns an empty result, not an error. |
| limit | integer | Max results. Default 20. |
| offset | integer | Pagination offset. Default 0. |
Rows are never
merged across platforms — you'll get one row per (story_id, platform). If Reddit reads
negative/high-controversy and Bluesky reads neutral on the same story, that disagreement is itself a
signal, so results keep it visible rather than averaging it away.
Using Echo from the Python SDK
As of polari-sdk
v0.3.0, Echo has a typed client.echo layer — no need to drop into raw httpx
alongside the SDK.
platform accepts a single string or a list on every SDK method that takes it — the SDK
normalizes it to a comma-separated query string. An inactive or unrecognized platform returns an empty
result rather than raising. search() raises ValueError client-side for an invalid
entity_scope, before any request goes out.
Freshness & coverage
Echo enrichment
is written per platform per story — one row for Reddit, one for Bluesky, one for Twitter/X. Each is updated
in place as new activity is detected. The fetched_at timestamp on each enrichment tells you
when the latest signal was collected.
| Platform | Update cadence | Signal type | Status |
|---|---|---|---|
| Inline with article fetch, plus targeted search | Comment threads on editorial posts, and threads found via query-driven search on any story | Live | |
| Bluesky | Every 30 minutes | Targeted post search driven by story entities | Live |
| X / Twitter | Every 6 hours | Targeted search on high/medium-priority clusters, ranked by engagement | Live |
Stories with no
Echo enrichment return a 404 from the single-story endpoint and are omitted from batch
responses. Not all clusters will have enrichment — Echo targets active, high-signal stories, and platform
coverage varies: Twitter's dataset is newer and smaller than Reddit's or Bluesky's as of this writing.