{"openapi":"3.1.0","info":{"title":"Quorumz Public API","description":"Geo-located citizen-journalism feed, regional topic+opinion digests (quorum), and tag/media access — the same data that powers quorumz.com, exposed for programmatic consumption.\n\n## What is a geobit?\n\nA **geobit** is one piece of place-anchored content (text, image, or video) ingested from a public source — a TikTok video, a Telegram channel post, a Bluesky post, a Mastodon toot, a Twitter status, a YouTube short, a regional-news RSS item, or an iWitness contribution submitted through the Quorumz app. Every geobit carries a `country_code`, optional `place_name` + `latitude`/`longitude`, a `published_at` platform-native timestamp, a `permalink` to the original post, and a content-addressed media pointer when the post had a photo or video.\n\n## Source layers\n\nEach geobit belongs to exactly one of three layers (used for facet filtering on the frontend and as evidence weights in the quorum cluster):\n\n- **`media`** — outlet/RSS sources, anything classified as a news outlet during ingestion (verified journalistic voice).\n- **`netizens`** — public posts from Telegram channels, TikTok creators, Twitter accounts, Bluesky/Mastodon users, Threads, Instagram, Aparat, etc. Volume-dominant, highest recency.\n- **`iwitness`** — Quorumz-app users who submitted a first-hand geobit signed by their EVM passkey (lowest volume, highest specificity).\n\n## Quorum (topic+opinion clustering)\n\nWorker containers cluster recent geobits per scope (country, region, or user-drawn bbox) into **topics** (\"what conversation is happening here\") with one or more **opinions** (\"how it splits\"). Each opinion carries a headline, summary, sentiment, supporting-geobit count, and the share of evidence per source layer. See the `/api/quorum/*` routes.\n\n## Auth\n\nAll `/api/*` data routes require `Authorization: Bearer <key>` (alias header: `X-Api-Key`). Issue a key by emailing **devops.admin@quorumz.com**; pricing per-call is published via the d402.net MCP wrapper of this API for self-service agent access.\n\n## Caching\n\nMost data routes set `Cache-Control: public, s-maxage=…` (60–300 s) and are CDN-friendly. The Cloudflare Workers frontend caches them at the edge with the same TTL, so your direct calls won't see edge hits unless you front us with your own CDN.","version":"1.0.0","contact":{"name":"Quorumz","email":"devops.admin@quorumz.com","url":"https://sense.quorumz.com/agents/docs"}},"paths":{"/healthz":{"get":{"tags":["system"],"summary":"Liveness probe","description":"Returns 200 with the service name + UTC timestamp when the API process is alive. Unauthenticated. Used by Caddy, container healthchecks, and uptime monitors.","operationId":"healthz_healthz_get","responses":{"200":{"description":"`{status: \"ok\", service, environment, timestamp}`","content":{"application/json":{"schema":{"additionalProperties":{"type":"string"},"type":"object","title":"Response Healthz Healthz Get"}}}}}}},"/api/stats/ingestion":{"get":{"tags":["stats"],"summary":"Ingestion Stats","operationId":"ingestion_stats_api_stats_ingestion_get","parameters":[{"name":"hours","in":"query","required":false,"schema":{"type":"integer","default":24,"title":"Hours"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/stats/grafana/by-country":{"get":{"tags":["stats"],"summary":"Grafana By Country","description":"Per-country ingestion over a time window. One row per (country,source).\n\nOutput columns: country_code, tier, source_id, admits, latest_admit_at.","operationId":"grafana_by_country_api_stats_grafana_by_country_get","parameters":[{"name":"window","in":"query","required":false,"schema":{"type":"string","default":"24h","title":"Window"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/stats/grafana/ingestion-monitor":{"get":{"tags":["stats"],"summary":"Grafana Ingestion Monitor","description":"Discovery + collection throughput monitor per platform/country.\n\nOutput rows are keyed by (source_id, country_code) and include:\n- discovery_runs / discovery_discovered / discovery_per_hour\n- collection_runs / collection_geobits / collection_per_hour\n- last_discovery_at / last_collection_at\n- health_status + health_reason (staleness/zero-throughput signals)","operationId":"grafana_ingestion_monitor_api_stats_grafana_ingestion_monitor_get","parameters":[{"name":"window","in":"query","required":false,"schema":{"type":"string","default":"24h","title":"Window"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/stats/grafana/ingestion-monitor-summary":{"get":{"tags":["stats"],"summary":"Grafana Ingestion Monitor Summary","description":"Summary counts of ingestion health by source_id for Grafana gauges.","operationId":"grafana_ingestion_monitor_summary_api_stats_grafana_ingestion_monitor_summary_get","parameters":[{"name":"window","in":"query","required":false,"schema":{"type":"string","default":"24h","title":"Window"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/stats/grafana/timeseries":{"get":{"tags":["stats"],"summary":"Grafana Timeseries","description":"Bucketed time series of admits. Output: [{ts, count, source_id, country_code}].\n\nbucket: 'hour' or 'day'. hours: lookback window (default 7d = 168h).\nFilterable by source_id and/or country_code.","operationId":"grafana_timeseries_api_stats_grafana_timeseries_get","parameters":[{"name":"source_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Source Id"}},{"name":"country_code","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Country Code"}},{"name":"bucket","in":"query","required":false,"schema":{"type":"string","default":"hour","title":"Bucket"}},{"name":"hours","in":"query","required":false,"schema":{"type":"integer","default":168,"title":"Hours"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/stats/grafana/rate-budgets":{"get":{"tags":["stats"],"summary":"Grafana Rate Budgets","description":"Current rate-budget utilization. One row per (platform, provider, window).","operationId":"grafana_rate_budgets_api_stats_grafana_rate_budgets_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/stats/grafana/source-health":{"get":{"tags":["stats"],"summary":"Grafana Source Health","description":"One row per known source — including silent/disabled ones.\n\nExisting ``ingestion-monitor`` only surfaces (source × country)\nrows that have at least one CountryProcessRun in the window, so\na fully-silent connector (zero runs, zero admits) doesn't appear\non the dashboard at all. This endpoint takes the union of every\nconfigured source — both active and intentionally-disabled — and\nattaches a coarse health label keyed only on freshness:\n\n  healthy   — admitted ≥1 row in the last hour of the window\n  warning   — admitted ≥1 row in the window but stalled >1h\n  critical  — zero admits in the window\n  disabled  — connector container is intentionally off\n\nOutput columns: source_id, status (active/disabled), enabled,\nadmits_window, latest_observed_at, minutes_since_latest,\nhealth, window.","operationId":"grafana_source_health_api_stats_grafana_source_health_get","parameters":[{"name":"window","in":"query","required":false,"schema":{"type":"string","default":"24h","title":"Window"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/debug/memory-snapshot":{"get":{"tags":["debug"],"summary":"Debug Memory Snapshot","description":"tracemalloc top allocators for the *current worker process*.\n\nEach uvicorn worker is its own Python process, so calls to this\nendpoint are load-balanced across workers and a single response\nonly reflects one process. Hit it 3-5x and look at which workers\nreturn the highest ``current_mb`` to compare against\ndocker stats memory percentages.\n\nQuery params:\n- ``limit`` (default 30): top-N allocation sites\n- ``group_by`` (default ``lineno``): one of ``lineno`` | ``filename`` | ``traceback``\n\nAuth: same x-grafana-token / ?token= contract as the grafana\nstats endpoints. Returns 503 if QUORUMZ_TRACEMALLOC is not set.","operationId":"debug_memory_snapshot_api_debug_memory_snapshot_get","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":30,"title":"Limit"}},{"name":"group_by","in":"query","required":false,"schema":{"type":"string","default":"lineno","title":"Group By"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/stats/grafana/iwitness":{"get":{"tags":["stats"],"summary":"Grafana Iwitness","description":"iWitness contribution stats: counts by status + by country.","operationId":"grafana_iwitness_api_stats_grafana_iwitness_get","parameters":[{"name":"window","in":"query","required":false,"schema":{"type":"string","default":"7d","title":"Window"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/media/object":{"get":{"tags":["media"],"summary":"Fetch the bytes of a content-addressed media pointer","description":"Returns the raw bytes for the media object referenced by `ptr`. The `ptr` is a content-addressed pointer of the form `gcs://<bucket>/<object>` (URL-encoded when used as a query string) — the same pointer always resolves to the same bytes.\n\n**This route is unauthenticated.** It is safe to embed directly in `<img src=…>` and `<video src=…>` from browsers — the pointer itself is the capability. Bytes are immutable once stored, so the response is marked `Cache-Control: public, max-age=31536000, immutable` and browsers + CDNs will cache aggressively.\n\nPointers come from the `preview_url` field on geobit records — feed responses already give you absolute URLs of the form `https://api.quorumz.com/api/media/object?ptr=…` so you typically don't need to construct this URL by hand.","operationId":"media_object_api_media_object_get","parameters":[{"name":"ptr","in":"query","required":true,"schema":{"type":"string","description":"Content-addressed media pointer. Comes from the `preview_url` field on a geobit (any feed/quorum response). Format: a URL-encoded `gcs://<bucket>/<object>` string. Same `ptr` always resolves to the same bytes — safe to cache forever client-side. Construct your own pointer only if you already know the GCS object key; otherwise just pass through what a feed item gave you.","examples":["gcs%3A%2F%2Fquorumz-media-derived-dev%2Ftelegram_public_channels%2FIL%2Fexample%2F12345%2Fexample_12345.jpg"],"title":"Ptr"},"description":"Content-addressed media pointer. Comes from the `preview_url` field on a geobit (any feed/quorum response). Format: a URL-encoded `gcs://<bucket>/<object>` string. Same `ptr` always resolves to the same bytes — safe to cache forever client-side. Construct your own pointer only if you already know the GCS object key; otherwise just pass through what a feed item gave you."}],"responses":{"200":{"description":"Raw image/video bytes with the original Content-Type.","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/geobits/feed/{country_code}":{"get":{"tags":["feed"],"summary":"Live geobit feed for one country","description":"Live-queried feed for a single ISO 3166-1 alpha-2 `country_code`. Returns the most recent up-to-`limit` geobits the classifier has accepted, ordered newest-first. **Use the GCS-backed `/api/feeds/countries/{cc}` if you want sub-second reads** — that route is pre-baked and edge-cacheable. This live route exists for cases where you need a per-`source_id` filter, want to require lat/lng (`require_place=true`), or want freshness tighter than the snapshot's 5-minute publish cadence.\n\nEach item in `items[]` carries: `geobit_id`, `country_code`, `source_id`, `permalink`, `content_type` (text|image|video), `place_name`, `latitude`, `longitude`, `published_at` (ISO-8601 UTC), `language`, `preview_url` (absolute media-object URL on api.quorumz.com), `evidence_tier` (A|B|C — geo-confidence ranking).","operationId":"geobit_feed_api_geobits_feed__country_code__get","parameters":[{"name":"country_code","in":"path","required":true,"schema":{"type":"string","minLength":2,"maxLength":2,"pattern":"^[A-Z]{2}$","description":"ISO 3166-1 alpha-2 country code in UPPERCASE. Discover available active countries from `GET /api/quorum/markers` → `countries[].id`. Common values: `IL`, `IR`, `US`, `LB`, `SY`, `JO`, `AE`, `SA`, `EG`, `DE`, `FR`, `GB`, `PK`.","examples":["IL","IR","US"],"title":"Country Code"},"description":"ISO 3166-1 alpha-2 country code in UPPERCASE. Discover available active countries from `GET /api/quorum/markers` → `countries[].id`. Common values: `IL`, `IR`, `US`, `LB`, `SY`, `JO`, `AE`, `SA`, `EG`, `DE`, `FR`, `GB`, `PK`."},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":200,"minimum":1,"description":"Max number of geobits to return, newest-first.","default":20,"title":"Limit"},"description":"Max number of geobits to return, newest-first."},{"name":"source_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter to a single source platform. One of the ingestion source IDs. Leave unset to include every platform.","examples":["telegram_public_channels","tiktok_public_videos","user_contributed"],"enum":["telegram_public_channels","tiktok_public_videos","twitter_public_posts","bluesky_public_posts","mastodon_public_posts","youtube_public_video","youtube_live_streams","regional_news_rss","threads_public_posts","instagram_public_content","user_contributed"],"title":"Source Id"},"description":"Filter to a single source platform. One of the ingestion source IDs. Leave unset to include every platform."},{"name":"require_place","in":"query","required":false,"schema":{"type":"boolean","description":"If `true`, only return geobits that have non-null `latitude`/`longitude` (i.e. point-resolved on a map).","default":false,"title":"Require Place"},"description":"If `true`, only return geobits that have non-null `latitude`/`longitude` (i.e. point-resolved on a map)."},{"name":"min_location_confidence","in":"query","required":false,"schema":{"anyOf":[{"type":"number","maximum":1.0,"minimum":0.0},{"type":"null"}],"description":"Drop geobits whose `location_confidence` is below this threshold (0.0–1.0). Confidence is the geo-resolver's score for how sure we are about the place_name → lat/lng mapping. Use ~0.7 for tight filtering, ~0.4 for loose.","examples":[0.7],"title":"Min Location Confidence"},"description":"Drop geobits whose `location_confidence` is below this threshold (0.0–1.0). Confidence is the geo-resolver's score for how sure we are about the place_name → lat/lng mapping. Use ~0.7 for tight filtering, ~0.4 for loose."},{"name":"category","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Comma-separated list of GeobitCategory values to include (allow-list). Invalid entries are silently dropped. Valid values: `conflict_war`, `protest_assembly`, `state_power_accountability`, `civil_rights_discrimination`, `cost_of_living_labor`, `public_services_infrastructure`, `environment_climate`, `health_disease`, `humanitarian_migration`, `public_safety_emergencies`, `local_civic_life`, `other_civic`.","examples":["conflict_war","protest_assembly,humanitarian_migration","environment_climate,public_safety_emergencies"],"title":"Category"},"description":"Comma-separated list of GeobitCategory values to include (allow-list). Invalid entries are silently dropped. Valid values: `conflict_war`, `protest_assembly`, `state_power_accountability`, `civil_rights_discrimination`, `cost_of_living_labor`, `public_services_infrastructure`, `environment_climate`, `health_disease`, `humanitarian_migration`, `public_safety_emergencies`, `local_civic_life`, `other_civic`."}],"responses":{"200":{"description":"`{country_code, count, items[]}` where each item is a geobit pointer record.","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Geobit Feed Api Geobits Feed  Country Code  Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/geobits/feed":{"get":{"tags":["feed"],"summary":"Live consolidated feed across all active countries","description":"Live-queried geobit feed across every country currently in the active set, capped at `limit_per_country` items per country. Use this for a single-shot global scan. The response is edge-cacheable for 60 s.\n\n**Prefer `/api/feeds/global` for SSR / agent calls** — that one is the GCS-backed pre-baked snapshot, ~1 s warm versus 5–15 s on this live path. This live route is appropriate when you need stricter freshness than the 5-minute snapshot cadence.","operationId":"geobit_feed_all_api_geobits_feed_get","parameters":[{"name":"limit_per_country","in":"query","required":false,"schema":{"type":"integer","maximum":500,"minimum":1,"description":"Cap on how many newest geobits to include per country. The response carries items from every active country; this controls each country's slice.","default":60,"title":"Limit Per Country"},"description":"Cap on how many newest geobits to include per country. The response carries items from every active country; this controls each country's slice."}],"responses":{"200":{"description":"`{count, countries[], items[]}` — `items` is a flat newest-first list across all countries.","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/geobits/feed-snapshot":{"get":{"tags":["feed"],"summary":"Two-tier merged snapshot (today + last 7 days)","description":"Returns the pre-baked feed snapshot — today's geobits merged with the last 7 days of historic items, sorted newest-first. This is the cheapest call to get a wide feed window in one trip.\n\nTwo backend rows compose the response:\n\n- `today` covers `published_at >= today_start_utc` and refreshes every 5 minutes\n- `historic` covers the prior 7 days and refreshes every 24 h\n\nEdge-cacheable for 5 min (`s-maxage=300`); one fetch per Cloudflare colo per 5 min serves every downstream client. Returns an empty payload (rather than 404) when the snapshot table is missing — the frontend falls through to live-query in that case.","operationId":"geobit_feed_snapshot_api_geobits_feed_snapshot_get","responses":{"200":{"description":"`{items[], count, countries[], today_updated_at, historic_updated_at}` — timestamps tell you the snapshot freshness so callers can decide whether to refetch.","content":{"application/json":{"schema":{}}}}}}},"/api/feeds/{scope}":{"get":{"tags":["feed"],"summary":"GCS-backed pre-baked feed snapshot for a scope","description":"**Recommended path for agents and SSR.** Returns a JSON snapshot pulled directly from GCS (`gs://quorumz-feeds-dev/feeds/v1/<scope>.json`) — no DB read, ~50 ms warm at the edge.\n\nThe publisher background job rewrites these objects every ~5 minutes from the same data that powers `/api/geobits/feed-snapshot`, so the freshness ceiling is the same.\n\n**Valid `scope` values:**\n\n- `global` — everything; the full feed (~6 MB)\n- `row` — rest-of-world only (excludes hot + baseline tiers); useful if you already have hot countries cached\n- `manifest` — small index listing every scope file with its `last_updated` ISO timestamp\n- `regions/<region-key>` — a named region (e.g. `regions/middle-east`); region keys are listed in `manifest`\n- `countries/<CC>` — single country, ISO 3166-1 alpha-2 uppercase (e.g. `countries/IL`)\n\nAnything else returns 400. The publisher only writes objects for active countries, so a country that has had no recent ingestion may 404; falling back to `/api/feeds/global` will always yield data.","operationId":"gcs_feed_scope_api_feeds__scope__get","parameters":[{"name":"scope","in":"path","required":true,"schema":{"type":"string","description":"Which pre-baked snapshot to read. Valid forms:\n\n- `global` — full feed across every active country\n- `row` — rest-of-world tier only (excludes hot + baseline)\n- `manifest` — small index listing every available scope file with its `last_updated` timestamp; call this first to discover which countries / regions currently have a published snapshot\n- `regions/<region-slug>` — one named region (e.g. `regions/middle-east`); valid slugs come from the `manifest` response or `GET /api/quorum/markers` → `regions_catalog[].slug`\n- `countries/<CC>` — one country, ISO 3166-1 alpha-2 UPPERCASE (e.g. `countries/IL`)","examples":["global","manifest","countries/IL","regions/middle-east","row"],"title":"Scope"},"description":"Which pre-baked snapshot to read. Valid forms:\n\n- `global` — full feed across every active country\n- `row` — rest-of-world tier only (excludes hot + baseline)\n- `manifest` — small index listing every available scope file with its `last_updated` timestamp; call this first to discover which countries / regions currently have a published snapshot\n- `regions/<region-slug>` — one named region (e.g. `regions/middle-east`); valid slugs come from the `manifest` response or `GET /api/quorum/markers` → `regions_catalog[].slug`\n- `countries/<CC>` — one country, ISO 3166-1 alpha-2 UPPERCASE (e.g. `countries/IL`)"}],"responses":{"200":{"description":"Same JSON shape as `/api/geobits/feed-snapshot`: `{items[], count, countries[], today_updated_at, historic_updated_at}`.","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/reader/request-code":{"post":{"tags":["reader"],"summary":"Reader Request Code Endpoint","operationId":"reader_request_code_endpoint_api_reader_request_code_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/reader/verify":{"post":{"tags":["reader"],"summary":"Reader Verify Endpoint","operationId":"reader_verify_endpoint_api_reader_verify_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/reader/me":{"get":{"tags":["reader"],"summary":"Reader Me Endpoint","operationId":"reader_me_endpoint_api_reader_me_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/auth/register":{"post":{"tags":["auth"],"summary":"Auth Register Endpoint","description":"Register a new anonymous user account with EVM address.","operationId":"auth_register_endpoint_api_auth_register_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/auth/store-keys":{"post":{"tags":["auth"],"summary":"Auth Store Keys Endpoint","description":"Store encrypted key material for session unlock and seed recovery.","operationId":"auth_store_keys_endpoint_api_auth_store_keys_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/auth/recovery/{username}":{"get":{"tags":["auth"],"summary":"Auth Recovery Endpoint","description":"Get encrypted seed bundle for alias-based passkey recovery.","operationId":"auth_recovery_endpoint_api_auth_recovery__username__get","parameters":[{"name":"username","in":"path","required":true,"schema":{"type":"string","title":"Username"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/auth/account/{evm_address}":{"get":{"tags":["auth"],"summary":"Auth Account Endpoint","description":"Get account info for an EVM address.","operationId":"auth_account_endpoint_api_auth_account__evm_address__get","parameters":[{"name":"evm_address","in":"path","required":true,"schema":{"type":"string","title":"Evm Address"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/auth/username/available/{username}":{"get":{"tags":["auth"],"summary":"Auth Username Available Endpoint","description":"Check if a username is available.","operationId":"auth_username_available_endpoint_api_auth_username_available__username__get","parameters":[{"name":"username","in":"path","required":true,"schema":{"type":"string","title":"Username"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Auth Username Available Endpoint Api Auth Username Available  Username  Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/geobits/contribute":{"post":{"tags":["contribute"],"summary":"Contribute Geobit Endpoint","description":"Submit an anonymous, cryptographically signed geobit.","operationId":"contribute_geobit_endpoint_api_geobits_contribute_post","requestBody":{"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_contribute_geobit_endpoint_api_geobits_contribute_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/geobits/reply":{"post":{"tags":["contribute"],"summary":"Reply Geobit Endpoint","description":"Post an anonymous iWitness comment on a stance or a specific geobit.","operationId":"reply_geobit_endpoint_api_geobits_reply_post","requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/Body_reply_geobit_endpoint_api_geobits_reply_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/social/connect/{platform}":{"get":{"tags":["social"],"summary":"Social Connect Endpoint","operationId":"social_connect_endpoint_api_social_connect__platform__get","parameters":[{"name":"platform","in":"path","required":true,"schema":{"type":"string","title":"Platform"}},{"name":"evm","in":"query","required":false,"schema":{"type":"string","default":"","title":"Evm"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/social/connect-brand/{platform}":{"get":{"tags":["social"],"summary":"Social Connect Brand Endpoint","description":"Admin-gated: mint the X authorize URL to re-consent the @xQuorumz brand\naccount. Gated by social_token_key (a server secret) so only an operator\nwith Secret Manager access can start a brand-flow consent. The callback\nadditionally verifies the authorized handle before persisting.","operationId":"social_connect_brand_endpoint_api_social_connect_brand__platform__get","parameters":[{"name":"platform","in":"path","required":true,"schema":{"type":"string","title":"Platform"}},{"name":"token","in":"query","required":false,"schema":{"type":"string","default":"","title":"Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/social/callback/{platform}":{"get":{"tags":["social"],"summary":"Social Callback Endpoint","operationId":"social_callback_endpoint_api_social_callback__platform__get","parameters":[{"name":"platform","in":"path","required":true,"schema":{"type":"string","title":"Platform"}},{"name":"code","in":"query","required":false,"schema":{"type":"string","default":"","title":"Code"}},{"name":"state","in":"query","required":false,"schema":{"type":"string","default":"","title":"State"}},{"name":"error","in":"query","required":false,"schema":{"type":"string","default":"","title":"Error"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/social/accounts":{"get":{"tags":["social"],"summary":"Social Accounts Endpoint","operationId":"social_accounts_endpoint_api_social_accounts_get","parameters":[{"name":"evm","in":"query","required":false,"schema":{"type":"string","default":"","title":"Evm"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/social/disconnect/{platform}":{"post":{"tags":["social"],"summary":"Social Disconnect Endpoint","operationId":"social_disconnect_endpoint_api_social_disconnect__platform__post","parameters":[{"name":"platform","in":"path","required":true,"schema":{"type":"string","title":"Platform"}}],"requestBody":{"required":true,"content":{"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/Body_social_disconnect_endpoint_api_social_disconnect__platform__post"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/social/reply":{"post":{"tags":["social"],"summary":"Social Reply Endpoint","description":"Post the user's reply on their OWN connected account, MIRROR it as an\nattributed Quorumz comment (when a target is given), then award a STAR.","operationId":"social_reply_endpoint_api_social_reply_post","requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/Body_social_reply_endpoint_api_social_reply_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/alpha/item-replies":{"get":{"tags":["alpha"],"summary":"Anonymous iWitness comments for a stance / geobit","operationId":"alpha_item_replies_endpoint_api_alpha_item_replies_get","parameters":[{"name":"reply_to_kind","in":"query","required":true,"schema":{"type":"string","title":"Reply To Kind"}},{"name":"reply_to_id","in":"query","required":true,"schema":{"type":"string","title":"Reply To Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/geobits/contribute/{contribution_id}":{"get":{"tags":["contribute"],"summary":"Get Contribution Status Endpoint","description":"Return current status of a contribution + per-reject guidance.\n\nUsed by the contribute UI to render the revision / dispute screen\nafter a submit returns 422 unlisted.","operationId":"get_contribution_status_endpoint_api_geobits_contribute__contribution_id__get","parameters":[{"name":"contribution_id","in":"path","required":true,"schema":{"type":"string","title":"Contribution Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/geobits/contribute/{contribution_id}/revise":{"post":{"tags":["contribute"],"summary":"Revise Contribution Endpoint","description":"Re-submit an unlisted contribution with updated text/media.\n\nReturns 200 with the admitted contribution if the revision now passes\nthe classifier. Returns 422 with updated reject_reason + guidance +\nremaining attempts if still rejected. Returns 403 when attempts\nexhausted (user must file a dispute).","operationId":"revise_contribution_endpoint_api_geobits_contribute__contribution_id__revise_post","parameters":[{"name":"contribution_id","in":"path","required":true,"schema":{"type":"string","title":"Contribution Id"}}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_revise_contribution_endpoint_api_geobits_contribute__contribution_id__revise_post"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/geobits/contribute/{contribution_id}/dispute":{"post":{"tags":["contribute"],"summary":"File Dispute Endpoint","description":"File a dispute on an unlisted contribution with ≥3 revisions.\n\nQueues the contribution for moderator review. v1: no moderation UI —\ndisputes accumulate in the DB until we ship one.","operationId":"file_dispute_endpoint_api_geobits_contribute__contribution_id__dispute_post","parameters":[{"name":"contribution_id","in":"path","required":true,"schema":{"type":"string","title":"Contribution Id"}}],"requestBody":{"required":true,"content":{"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/Body_file_dispute_endpoint_api_geobits_contribute__contribution_id__dispute_post"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/tags/suggest":{"get":{"tags":["tags"],"summary":"Tag autocomplete sorted by popularity","description":"Returns up-to-`limit` tags whose name starts with the case-insensitive prefix `q`, ranked by how many geobits currently carry that tag. Use this for typeahead UIs and for agents searching for evidence on a topic.\n\nTags come from two sources: the geobit classifier (content-extracted, e.g. `earthquake`, `protest`, `election`) and iWitness contributors (free-text per submission). Both are normalized to lowercase, alphanumeric + hyphens, and merged into one ranked list.\n\n`limit` is capped at 50 server-side; supply a smaller value if you want a tight typeahead.","operationId":"tag_suggest_endpoint_api_tags_suggest_get","parameters":[{"name":"q","in":"query","required":false,"schema":{"type":"string","maxLength":64,"description":"Prefix to match (case-insensitive). Tags are stored lowercase, alphanumeric + hyphens. Empty string returns the most-used tags overall, useful as a discovery starting point. Try short stems like `ir`, `prot`, `earth`.","examples":["ir","protest","earthquake","election","gaza"],"default":"","title":"Q"},"description":"Prefix to match (case-insensitive). Tags are stored lowercase, alphanumeric + hyphens. Empty string returns the most-used tags overall, useful as a discovery starting point. Try short stems like `ir`, `prot`, `earth`."},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":50,"minimum":1,"description":"How many ranked suggestions to return. Server caps at 50.","default":10,"title":"Limit"},"description":"How many ranked suggestions to return. Server caps at 50."}],"responses":{"200":{"description":"`{tags: [{tag: string, usage_count: int}]}`.","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/layers/opensky":{"get":{"tags":["layers"],"summary":"OpenSky live aircraft states (relay)","description":"Relay endpoint for OpenSky `/states/all`. Lives here because Cloudflare Workers' egress to OpenSky hangs (rate-limited shared IPs); our VM's dedicated egress IP completes in under a second.\n\nAccepts the standard OpenSky bbox params: `lamin`, `lomin`, `lamax`, `lomax` (south, west, north, east decimal degrees). Trimmed server-side: only airborne aircraft are returned, with only the render-relevant fields (icao, callsign, origin, lat, lng, altitude, velocity, heading).","operationId":"opensky_relay_endpoint_api_layers_opensky_get","responses":{"200":{"description":"`{time, states[]}` — OpenSky time + trimmed state list.","content":{"application/json":{"schema":{}}}}}}},"/api/layers/bootstrap-snapshot":{"get":{"tags":["layers"],"summary":"Combined snapshot of all map layers","description":"Single composite payload that bundles every map-layer feed in one trip — flights (OpenSky), ships (AISStream), live webcams (Windy), earthquakes (USGS), wildfires (NASA EONET), severe-weather alerts (NOAA), GDACS multi-hazard alerts, ACLED conflict events, plus rain radar (RainViewer) tile-host metadata.\n\nThe backend refreshes each constituent layer independently every few minutes — this endpoint composes them on read so a single fetch lets a client paint every layer at once.\n\nThis is the only layer endpoint the frontend should call for first paint. Per-layer endpoints (`/api/layers/opensky`, etc.) exist for special cases.","operationId":"layer_bootstrap_snapshot_endpoint_api_layers_bootstrap_snapshot_get","responses":{"200":{"description":"`{refreshed_at, flights[], ships[], webcams[], usgs_features[], nasa_eonet_events[], noaa_alerts[], gdacs_alerts[], acled_events[], rainviewer: {host, latest_past_path}}` — empty arrays for layers that haven't refreshed yet.","content":{"application/json":{"schema":{}}}}}}},"/api/feedback":{"post":{"tags":["feedback"],"summary":"Submit Feedback Endpoint","description":"Submit user feedback. Anonymous by default; ``contact_handle``\noptional. Body fields: ``body`` (required), ``page_path``,\n``locale``, ``country_code``, ``contact_handle``, ``metadata``.","operationId":"submit_feedback_endpoint_api_feedback_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/geobits/{contribution_id}/confirm":{"post":{"tags":["confirm"],"summary":"Confirm Geobit Endpoint","description":"Confirm (heart) a geobit. Requires evm_address and signature.","operationId":"confirm_geobit_endpoint_api_geobits__contribution_id__confirm_post","parameters":[{"name":"contribution_id","in":"path","required":true,"schema":{"type":"string","title":"Contribution Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["confirm"],"summary":"Unconfirm Geobit Endpoint","description":"Remove a confirmation. Requires evm_address and signature.","operationId":"unconfirm_geobit_endpoint_api_geobits__contribution_id__confirm_delete","parameters":[{"name":"contribution_id","in":"path","required":true,"schema":{"type":"string","title":"Contribution Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/geobits/{contribution_id}/confirmations":{"get":{"tags":["confirm"],"summary":"Get Confirmations Endpoint","description":"List confirmations for a geobit (public read).","operationId":"get_confirmations_endpoint_api_geobits__contribution_id__confirmations_get","parameters":[{"name":"contribution_id","in":"path","required":true,"schema":{"type":"string","title":"Contribution Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Confirmations Endpoint Api Geobits  Contribution Id  Confirmations Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/quorum/markers":{"get":{"tags":["quorum"],"summary":"Lightweight scope index — which countries/regions have a digest","description":"Tiny index (≈10 KB) of every country and region that has a recent quorum run. Use this as a first call to discover what's available before requesting the heavier `/quorum/all` bundle or a specific `/quorum/country` / `/quorum/region` payload.\n\nReturns `countries[]` and `regions[]` — each entry has `{id, label, topic_count, geobit_count, center: [lat, lon], generated_at}`. Plus a `regions_catalog[]` that lists the canonical regions with their member countries for navigation.\n\nEdge-cacheable for 5 minutes.","operationId":"quorum_markers_endpoint_api_quorum_markers_get","responses":{"200":{"description":"`{countries[], regions[], regions_catalog[]}` — markers carry counts + center coords so a map glyph layer can render directly without a second call.","content":{"application/json":{"schema":{}}}}}}},"/api/quorum/all":{"get":{"tags":["quorum"],"summary":"Full quorum bundle — markers + every country + every region (with inline geobits)","description":"Composite payload bundling every per-country and per-region quorum digest into one trip. Costs ~700 KB cold but lets a client paint the whole drill-down tree without N round-trips.\n\nEach opinion in the payload includes up to **50 inlined geobits** (truncated to 600 chars of text) so the panel can show evidence immediately without per-opinion drill-down calls. When the inline cap is exceeded, the panel falls back to `/quorum/topic/{tid}/opinions/{oid}/geobits` for paginated reads.\n\nCold path can take ~12 s (geocoder warmup + per-opinion geobit hydration); warm path serves from edge cache in ~50–150 ms. Refreshed when any country tier worker finishes.","operationId":"quorum_all_endpoint_api_quorum_all_get","responses":{"200":{"description":"`{markers, countries{CC: payload}, regions{slug: payload}, generated_at}` — countries and regions are keyed by id so a client can index O(1) into the scope the user clicked.","content":{"application/json":{"schema":{}}}}}}},"/api/quorum/country/{country_code}":{"get":{"tags":["quorum"],"summary":"Topic+opinion digest for one country","description":"Returns the latest quorum run for the given ISO 3166-1 alpha-2 `country_code`. Best for the question \"what is the conversation in <country> right now and how is it split?\"\n\n**Refresh cadence** depends on the country's tier:\n- **hot** countries (IL, IR, US, LB, …): every 15–30 min\n- **baseline** countries (DE, FR, GB, AE, …): every 1–2 h\n- **rest-of-world**: every ≤12 h\n\nThe country tier is metadata on the response (`tier`); use it to decide your own polling interval.\n\n**Returns 404** with a `reason: \"no_run_yet\"` body when a country has not yet had its first run (newly-onboarded country, or worker fleet just restarted).","operationId":"quorum_country_endpoint_api_quorum_country__country_code__get","parameters":[{"name":"country_code","in":"path","required":true,"schema":{"type":"string","minLength":2,"maxLength":2,"pattern":"^[A-Z]{2}$","description":"ISO 3166-1 alpha-2 country code in UPPERCASE. The set of countries with a current quorum run is discoverable via `GET /api/quorum/markers` → `countries[].id`. 404 if no run exists yet for that country.","examples":["IL","IR","US"],"title":"Country Code"},"description":"ISO 3166-1 alpha-2 country code in UPPERCASE. The set of countries with a current quorum run is discoverable via `GET /api/quorum/markers` → `countries[].id`. 404 if no run exists yet for that country."}],"responses":{"200":{"description":"`{scope: {kind: \"country\", id: CC}, topics[], geobit_count, confidence, generated_at, run_id, tier}`. Each topic has `{id, headline, summary, opinions[]}`; each opinion has `{id, summary, sentiment, supporting_geobit_count, facets: {media, netizens, iwitness}}`.","content":{"application/json":{"schema":{}}}},"404":{"description":"No quorum run exists for this country yet"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/quorum/region/{slug}":{"get":{"tags":["quorum"],"summary":"Topic+opinion digest for one region","description":"Same shape as `/quorum/country/{cc}` but scoped to a named region (e.g. `middle-east`, `south-asia`, `mena`).\n\nRegion slugs and their member countries are listed in `/quorum/markers` → `regions_catalog`.\n\nRegion runs aggregate across the constituent countries' geobits in a single LLM call so the topics+opinions represent cross-border conversation rather than the union of per-country digests.","operationId":"quorum_region_endpoint_api_quorum_region__slug__get","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","pattern":"^[a-z0-9-]+$","description":"Region slug — lowercase, hyphenated. Discover available region slugs from `GET /api/quorum/markers` → `regions_catalog[].slug` (each entry lists its member countries). 404 if no run exists yet for that region.","examples":["middle-east","south-asia","mena"],"title":"Slug"},"description":"Region slug — lowercase, hyphenated. Discover available region slugs from `GET /api/quorum/markers` → `regions_catalog[].slug` (each entry lists its member countries). 404 if no run exists yet for that region."}],"responses":{"200":{"description":"`{scope: {kind: \"region\", id: slug}, topics[], geobit_count, confidence, generated_at, run_id}`. Same topic/opinion structure as country.","content":{"application/json":{"schema":{}}}},"404":{"description":"No quorum run exists for this region yet"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/quorum/area":{"get":{"tags":["quorum"],"summary":"On-demand topic digest for an arbitrary bounding box","description":"Slow path — runs a synchronous LLM clustering call over geobits whose `latitude`/`longitude` fall inside `bbox` when no cached run exists for the requested cell. **Cache miss can take up to 60 s.**\n\n**`bbox` format**: `south,west,north,east` as decimal degrees, e.g. `bbox=29.0,34.0,33.5,36.5` for southern Israel + Sinai. Coordinates are validated for plausibility (latitudes in [-90,90], longitudes in [-180,180], south < north, west < east).\n\nBbox values are quantized to 0.5° cells server-side, so a user panning 0.1° hits the same cached row. Use this when the user has drawn a custom area on a map; for country/region scopes prefer the dedicated routes (faster, no LLM on hot path).\n\nPass `max_age_seconds` to override the default TTL (useful if you want fresher data and accept a slower cold path).","operationId":"quorum_area_endpoint_api_quorum_area_get","parameters":[{"name":"bbox","in":"query","required":true,"schema":{"type":"string","pattern":"^-?\\d+(?:\\.\\d+)?,-?\\d+(?:\\.\\d+)?,-?\\d+(?:\\.\\d+)?,-?\\d+(?:\\.\\d+)?$","description":"Bounding box as `south,west,north,east` decimal degrees. **Order matters** — south/west are the lower-left corner, north/east are the upper-right. Latitudes must be in [-90, 90], longitudes in [-180, 180], with `south < north` and `west < east`. Quantized to 0.5° cells server-side, so a user panning < 0.5° hits the same cached row.\n\nPick coordinates from a map — there's no API to discover \"interesting\" boxes; this endpoint is intended for user-drawn areas.","examples":["29.0,34.0,33.5,36.5","31.0,34.0,33.0,36.0","-23.0,113.0,-10.0,154.0"],"title":"Bbox"},"description":"Bounding box as `south,west,north,east` decimal degrees. **Order matters** — south/west are the lower-left corner, north/east are the upper-right. Latitudes must be in [-90, 90], longitudes in [-180, 180], with `south < north` and `west < east`. Quantized to 0.5° cells server-side, so a user panning < 0.5° hits the same cached row.\n\nPick coordinates from a map — there's no API to discover \"interesting\" boxes; this endpoint is intended for user-drawn areas."},{"name":"max_age_seconds","in":"query","required":false,"schema":{"anyOf":[{"type":"integer","minimum":0},{"type":"null"}],"description":"Override the default cache TTL (seconds). Set to a smaller value when you want a fresher run and accept paying the ≤60 s cold-path LLM call. Leave unset to use the server default (`settings.quorum_area_cache_seconds`).","examples":[60,300,1800],"title":"Max Age Seconds"},"description":"Override the default cache TTL (seconds). Set to a smaller value when you want a fresher run and accept paying the ≤60 s cold-path LLM call. Leave unset to use the server default (`settings.quorum_area_cache_seconds`)."}],"responses":{"200":{"description":"Same `{scope, topics[], geobit_count, confidence, generated_at}` shape as country/region, with `scope.kind = \"area\"` and `scope.bbox` echoing the quantized cell.","content":{"application/json":{"schema":{}}}},"400":{"description":"Invalid bbox (parse error or out of range)"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/quorum/topic/{topic_id}/opinions/{opinion_id}/geobits":{"get":{"tags":["quorum"],"summary":"Paginated geobits supporting one opinion","description":"Returns the geobits that the LLM clustering classified under a specific (topic, opinion) pair, paginated. Use this when the inline 50-geobit cap on the bundle/country/region payload isn't enough.\n\n`topic_id` and `opinion_id` come from a prior `/quorum/all` or `/quorum/country` / `/quorum/region` response — they are stable across runs only within the lifetime of the topic (currently ~hours; future versions may stitch topic continuity across runs via headline-embedding similarity).\n\nPagination: `limit` defaults to 50, capped at 200; `offset` defaults to 0. Total count is returned so the client can decide whether to keep paging.","operationId":"quorum_opinion_geobits_endpoint_api_quorum_topic__topic_id__opinions__opinion_id__geobits_get","parameters":[{"name":"topic_id","in":"path","required":true,"schema":{"type":"string","pattern":"^[a-z0-9_]+$","description":"Topic identifier from a prior quorum response. Source: any of `GET /api/quorum/all` → `countries.{cc}.topics[].id` (or `regions.{slug}`), or per-scope `GET /api/quorum/country/{cc}` → `topics[].id`. Stable within a topic's lifetime (~hours).","examples":["topic_il_2026_05_06_001","topic_us_2026_05_06_004"],"title":"Topic Id"},"description":"Topic identifier from a prior quorum response. Source: any of `GET /api/quorum/all` → `countries.{cc}.topics[].id` (or `regions.{slug}`), or per-scope `GET /api/quorum/country/{cc}` → `topics[].id`. Stable within a topic's lifetime (~hours)."},{"name":"opinion_id","in":"path","required":true,"schema":{"type":"string","pattern":"^[a-z0-9_]+$","description":"Opinion identifier inside the parent topic. Source: `topics[].opinions[].id` from any quorum payload (country/region/all). Always paired with its containing `topic_id`.","examples":["opinion_il_2026_05_06_001_a"],"title":"Opinion Id"},"description":"Opinion identifier inside the parent topic. Source: `topics[].opinions[].id` from any quorum payload (country/region/all). Always paired with its containing `topic_id`."},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":200,"minimum":1,"description":"Page size, capped at 200 server-side. Default 50 matches the inline-geobit cap in the parent quorum payload — only page beyond that when the bundle didn't already give you what you need.","default":50,"title":"Limit"},"description":"Page size, capped at 200 server-side. Default 50 matches the inline-geobit cap in the parent quorum payload — only page beyond that when the bundle didn't already give you what you need."},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0,"description":"Zero-based offset for pagination. Add `limit` each page; stop when the response's `total_count` is reached.","default":0,"title":"Offset"},"description":"Zero-based offset for pagination. Add `limit` each page; stop when the response's `total_count` is reached."}],"responses":{"200":{"description":"`{topic_id, opinion_id, geobits[], total_count, limit, offset}` — each geobit is the same shape as items in `/api/feeds/*` (permalink, content_type, place_name, lat/lng, published_at, preview_url).","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/alpha/all":{"get":{"tags":["alpha"],"summary":"All active alpha signals (geobits / opinions / topics tagged as new info)","description":"Returns every currently-active alpha signal — entities the alpha-worker has flagged as carrying new or counter-factual information not yet portrayed by mainstream media. Each row carries a `preview` payload sized for direct frontend render (geobit card, opinion card, or topic card depending on `entity_kind`).\n\nActive = `expires_at` not yet passed (default TTL 72 h). Edge-cached for 4 minutes. Pre-baked to `gs://<bucket>/alpha/v1/bundle.json` every 120 s by the publishers container.","operationId":"alpha_all_endpoint_api_alpha_all_get","parameters":[{"name":"max_age_minutes","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Max Age Minutes"}},{"name":"detected_since","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Detected Since"}}],"responses":{"200":{"description":"`{signals[], count, generated_at}` — each signal: `{signal_id, entity_kind, entity_id, country, lat, lon, alpha_kind, alpha_score, confidence, rationale, media_gap, detected_at, expires_at, preview}`. `alpha_score` (0..1) = how NEW/under-reported the signal is; `confidence` (0..1, null until the source quorum is re-scored) = how WELL-SOURCED it is (#posts × account credibility). Gate on alpha_score, scale conviction by confidence — see public-api-spec §12.\n\nFRESH-ONLY: pass `?max_age_minutes=N` (or `?detected_since=<ISO8601>`) to get only signals detected within the window — lead-time consumers (e.g. trading agents) avoid polling the full 72 h set for the fresh needles. `detected_at` is the FIRST-detection time (preserved across re-evaluation).","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/alpha/match":{"post":{"tags":["alpha"],"summary":"Semantically rank active alpha signals against a free-text query","description":"Body `{text, top_k?}`. Embeds `text` (e.g. a prediction-market question) and the active alpha signals with the same BGE-M3 model, and returns the signals ranked by cosine similarity — match by MEANING, not keywords. The caller stores no vectors and needs no model. Each match is the full `/alpha/all` signal shape plus `similarity` ∈ [0,1]. 503 `embeddings_unavailable` if the embedding service is down (fall back to your own matching).","operationId":"alpha_match_endpoint_api_alpha_match_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlphaMatchRequest"}}},"required":true},"responses":{"200":{"description":"`{matches[], count, query, generated_at}` — each match adds `similarity`.","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/alpha/feed":{"get":{"tags":["alpha"],"summary":"Unified signal feed (alpha + pf-shift + new-quorum + earthquake)","description":"The quorumz.com/alpha newspaper feed: every fresh temporal signal merged into one time-ordered list, newest first. Merges active alpha signals (which include public-figure stance shifts as entity_kind=pf_topic_shift), recently-formed quorum topics, and USGS earthquakes (M4.5+). Each item carries `{id, kind, title, summary, lat, lng, country_code, occurred_at, layer_source, deep_link, refs}`. Pre-baked to `gs://<bucket>/alpha-feed/v1/feed.json` every 120 s by the publishers container; this endpoint is the live fallback.","operationId":"alpha_feed_endpoint_api_alpha_feed_get","responses":{"200":{"description":"`{items[], generated_at, counts}`","content":{"application/json":{"schema":{}}}}}}},"/api/canonical/feed":{"get":{"tags":["alpha"],"summary":"Topic-level /alpha feed (canonical topics, newest activity first)","description":"The topic-object feed: every active canonical topic (unifying quorum opinions + public-figure stances), newest activity first, each with a representative city-level geo point, cover image, member counts, and an alpha flag. Powers the /alpha map+feed.","operationId":"canonical_feed_endpoint_api_canonical_feed_get","responses":{"200":{"description":"`{items[], generated_at, count}`","content":{"application/json":{"schema":{}}}}}}},"/api/canonical/topic/{canonical_id}":{"get":{"tags":["alpha"],"summary":"Full canonical-topic article (quorum + PF + alphas + comments)","description":"Aggregates everything for one canonical topic's article page: each member quorum topic's opinions + geobits, each member PF topic's stances + shift timeline + geobits, pertaining alpha signals, and aggregated iWitness comments.","operationId":"canonical_topic_endpoint_api_canonical_topic__canonical_id__get","parameters":[{"name":"canonical_id","in":"path","required":true,"schema":{"type":"string","title":"Canonical Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/alpha/item-meta":{"get":{"tags":["alpha"],"summary":"OpenGraph card meta for a quorum topic / PF (for link unfurls)","description":"Returns {title, description, image} for a topic_id or pf_id so the discovery page can emit an OG card — auto-tweets linking to /discover?topic=… then unfurl with the topic media image instead of bland text. Cheap single-entity lookup.","operationId":"alpha_item_meta_endpoint_api_alpha_item_meta_get","parameters":[{"name":"topic_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Topic Id"}},{"name":"pf_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pf Id"}},{"name":"opinion_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Opinion Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/alpha/item-geobits":{"get":{"tags":["alpha"],"summary":"Supporting geobits for one /alpha feed item (middle column)","description":"Resolves the supporting geobits behind a single /alpha feed item — what the page's middle column renders when an item is selected. Pass topic_id (+opinion_id) for quorum/alpha items, or pf_id (+topic_id) for a public-figure stance shift. Returns `{items[], count, source}` with each geobit shaped like the discovery feed card (text, preview_url, permalink, source_id, lat/lng, …).","operationId":"alpha_item_geobits_endpoint_api_alpha_item_geobits_get","parameters":[{"name":"topic_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Topic Id"}},{"name":"opinion_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Opinion Id"}},{"name":"pf_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pf Id"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":60,"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/alpha/markers":{"get":{"tags":["alpha"],"summary":"Alpha map glyph index (lat/lon + alpha_kind only)","description":"Lightweight version of `/alpha/all` carrying only what the map glyph layer needs — coordinates, alpha_kind, score. Use for the discover-map glyph render; fetch the full bundle on first panel open.","operationId":"alpha_markers_endpoint_api_alpha_markers_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/alpha/signal/{signal_id}":{"get":{"tags":["alpha"],"summary":"One alpha signal by id (drill-down fallback)","description":"Direct read of a single alpha signal. The bundle already carries every active signal inline so this is mostly a fallback for deep-linking to a specific signal_id.","operationId":"alpha_signal_endpoint_api_alpha_signal__signal_id__get","parameters":[{"name":"signal_id","in":"path","required":true,"schema":{"type":"string","title":"Signal Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"404":{"description":"Signal not found or archived"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/alpha/signal/{signal_id}/translation":{"get":{"tags":["alpha"],"summary":"English translation of a find's claim (the source post's words)","operationId":"alpha_signal_translation_endpoint_api_alpha_signal__signal_id__translation_get","parameters":[{"name":"signal_id","in":"path","required":true,"schema":{"type":"string","title":"Signal Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/alpha/webhooks":{"get":{"tags":["alpha"],"summary":"List your registered alpha webhooks","operationId":"list_alpha_webhooks_api_alpha_webhooks_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"401":{"description":"Missing or invalid API key"}}},"post":{"tags":["alpha"],"summary":"Register a webhook for new alpha signals","description":"Register an HTTPS endpoint to receive every newly-created alpha signal (event 'alpha.created'), HMAC-SHA256 signed with a per-subscription secret returned exactly once in this response. Requires a valid API key.","operationId":"register_alpha_webhook_api_alpha_webhooks_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookRegisterRequest"}}},"required":true},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"400":{"description":"Invalid or SSRF-unsafe URL / bad request"},"401":{"description":"Missing or invalid API key"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/alpha/webhooks/{subscription_id}":{"delete":{"tags":["alpha"],"summary":"Deactivate one of your webhooks","operationId":"delete_alpha_webhook_api_alpha_webhooks__subscription_id__delete","parameters":[{"name":"subscription_id","in":"path","required":true,"schema":{"type":"string","title":"Subscription Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"401":{"description":"Missing or invalid API key"},"404":{"description":"Not found or not owned by caller"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/public-figures/all":{"get":{"tags":["public-figures"],"summary":"Full public-figures bundle (figures + topics + opinions + recent shifts)","description":"One-shot read of the entire active public-figure roster (currently ~510 figures) with every figure's topics, opinions, inline supporting geobits, and stance-shift history. Pre-baked every 120 s and edge-cached for 240 s, so warm response is ~50–200 ms.\n\n**Response shape**: `{figures: {pf_id: figure_payload}, categories: {cat_key: {figures: [pf_id, ...]}}, all_figures: [pf_id, ...], generated_at}`.\n\nEach `figure_payload` carries `display_name`, `role`, `org`, `photo_url`, `primary_country_code`, `primary_category`, `lat`/`lon` (capital-jittered), `topics[]`, `recent_shifts[]`, `active_shifts[]`, `recent_shift` (latest, may be null), and the boolean flags `had_recent_shift` (365-day window) and `is_active_shift` (30-day window) that drive the map-glyph animation on quorumz.com.\n\nCost-conscious agents should prefer `/markers` for discovery, `/figure/{pf_id}` for a single read, and `/shifts` for just the flips.","operationId":"public_figures_all_endpoint_api_public_figures_all_get","responses":{"200":{"description":"`{figures, categories, all_figures, generated_at}`. Bundle size ~3–4 MB; edge-cached so latency is fine.","content":{"application/json":{"schema":{}}}}}}},"/api/public-figures/markers":{"get":{"tags":["public-figures"],"summary":"Lightweight per-figure index for map glyphs + shift discovery","description":"Strips the full bundle down to one compact entry per figure (~150 bytes each, total ~80 KB for ~510 figures). Use this as a first call to discover what's available before pulling the full `/all` bundle or a specific `/figure/{pf_id}` payload.\n\nEach marker carries: `pf_id`, `display_name`, `role`, `org`, `photo_url`, `primary_category`, `secondary_categories[]`, `primary_country_code`, `priority`, `lat`/`lon` (capital-jittered, may be null for stateless figures), `topic_count`, and the shift signal — `had_recent_shift` (any flip within 365 d), `is_active_shift` (within 30 d), `recent_shift_rationale` (one-line human summary), `recent_shift_at`, `recent_shift_age_days`.\n\nEdge-cacheable for 4 minutes.","operationId":"public_figures_markers_endpoint_api_public_figures_markers_get","responses":{"200":{"description":"`{markers: [{pf_id, display_name, …, recent_shift_*}], generated_at}`.","content":{"application/json":{"schema":{}}}}}}},"/api/public-figures/figure/{pf_id}":{"get":{"tags":["public-figures"],"summary":"One figure's full payload","description":"Deep-link to a single public figure. Returns the same shape as one entry of `/all`'s `figures[pf_id]`: full topic + opinion roster, shift history, social handles, and inline supporting geobits.\n\nPulled from the same in-memory bundle; ~50–200 ms warm. Edge-cached for 5 minutes.\n\nReturns 404 with `{\"error\": \"not_found\"}` when the figure is unknown or paused.","operationId":"public_figures_figure_endpoint_api_public_figures_figure__pf_id__get","parameters":[{"name":"pf_id","in":"path","required":true,"schema":{"type":"string","pattern":"^pf_[a-z0-9_]+$","description":"Public-figure identifier. Format `pf_<snake_case_name>` (e.g. `pf_isaac_herzog`, `pf_vladimir_putin`, `pf_hibatullah_akhundzada`). Discover available values from `GET /api/public-figures/markers` → `markers[].pf_id` or `GET /api/public-figures/all` → `all_figures[]`.","examples":["pf_isaac_herzog","pf_vladimir_putin","pf_hibatullah_akhundzada"],"title":"Pf Id"},"description":"Public-figure identifier. Format `pf_<snake_case_name>` (e.g. `pf_isaac_herzog`, `pf_vladimir_putin`, `pf_hibatullah_akhundzada`). Discover available values from `GET /api/public-figures/markers` → `markers[].pf_id` or `GET /api/public-figures/all` → `all_figures[]`."}],"responses":{"200":{"description":"Single-figure payload: `{pf_id, display_name, role, org, photo_url, primary_country_code, primary_category, secondary_categories, lat, lon, topics, recent_shifts, active_shifts, recent_shift, had_recent_shift, is_active_shift, influence_score, relevance_score, handles, …}`.","content":{"application/json":{"schema":{}}}},"404":{"description":"Figure not found or paused"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/public-figures/shifts":{"get":{"tags":["public-figures"],"summary":"Recent stance-shift digest across all public figures","description":"Focused list of public figures who have **flipped stance** within the freshness window — the cheap call when you specifically want \"who changed their mind recently\" without pulling the full 3–4 MB bundle. Newest flips first; ties broken by influence score so high-impact shifts (heads of state, central-bank chiefs) bubble up.\n\nEach entry combines figure metadata (`pf_id`, `display_name`, `role`, `org`, `photo_url`, `primary_country_code`, `primary_category`) with the shift specifics (`topic_id`, `topic_headline`, `from_opinion_id`, `from_stance_label`, `to_opinion_id`, `to_stance_label`, `detected_at`, `flip_age_days`, `rationale`).\n\nPulled from the same in-memory bundle the rest of the `/public-figures/*` routes use, so cost is minimal and edge-cacheable for 4 minutes.","operationId":"public_figures_shifts_endpoint_api_public_figures_shifts_get","parameters":[{"name":"days","in":"query","required":false,"schema":{"type":"integer","maximum":365,"minimum":1,"description":"Freshness window in days. Default 30 mirrors the `is_active_shift` flag (which drives the flicker-orb animation on quorumz.com). Use 365 to get all recently-detected shifts; the detector itself only looks at the latest two opinions per topic so older flips fall off.","examples":[30,7,90,365],"default":30,"title":"Days"},"description":"Freshness window in days. Default 30 mirrors the `is_active_shift` flag (which drives the flicker-orb animation on quorumz.com). Use 365 to get all recently-detected shifts; the detector itself only looks at the latest two opinions per topic so older flips fall off."},{"name":"country_code","in":"query","required":false,"schema":{"anyOf":[{"type":"string","minLength":2,"maxLength":2,"pattern":"^[A-Z]{2}$"},{"type":"null"}],"description":"Optional ISO 3166-1 alpha-2 filter on the figure's `primary_country_code`. Discover available country codes from `/public-figures/markers`.","examples":["IL","IR","US","RU"],"title":"Country Code"},"description":"Optional ISO 3166-1 alpha-2 filter on the figure's `primary_country_code`. Discover available country codes from `/public-figures/markers`."},{"name":"category","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":64},{"type":"null"}],"description":"Optional category filter; matches either the figure's `primary_category` or any `secondary_categories[]`. Discover available categories from `/public-figures/markers` (each marker carries them). Common values: `head_of_state`, `minister`, `parliamentarian`, `judge`, `central_banker`, `ceo`, `religious_leader`.","examples":["head_of_state","central_banker","ceo"],"title":"Category"},"description":"Optional category filter; matches either the figure's `primary_category` or any `secondary_categories[]`. Discover available categories from `/public-figures/markers` (each marker carries them). Common values: `head_of_state`, `minister`, `parliamentarian`, `judge`, `central_banker`, `ceo`, `religious_leader`."},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":500,"minimum":1,"description":"Cap on returned entries. Server-side max is 500. Use a tight value (≤20) for typeahead-style summary; raise it when scanning a quiet window.","default":50,"title":"Limit"},"description":"Cap on returned entries. Server-side max is 500. Use a tight value (≤20) for typeahead-style summary; raise it when scanning a quiet window."}],"responses":{"200":{"description":"`{shifts: [{pf_id, …shift_fields}], total, window_days, filters: {country_code, category}, generated_at}`. The `shifts` array is already filtered + sorted newest-first.","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/public-figures/audit/queue":{"get":{"tags":["public-figures"],"summary":"Pending-handle audit queue (operator UI)","description":"Returns up to `limit` figures with un-verified handles, grouped per figure with reference URLs (Wikipedia page, Google search, platform profile URLs) the operator can use to verify externally. Gated by the X-Admin-Token header — 401 when missing or wrong.","operationId":"public_figures_audit_queue_endpoint_api_public_figures_audit_queue_get","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":50,"title":"Limit"}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","default":0,"title":"Offset"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/public-figures/audit/verify":{"post":{"tags":["public-figures"],"summary":"Mark handles as verified=True (operator UI)","operationId":"public_figures_audit_verify_endpoint_api_public_figures_audit_verify_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/public-figures/audit/reject":{"post":{"tags":["public-figures"],"summary":"Mark handles status='removed' (operator UI)","operationId":"public_figures_audit_reject_endpoint_api_public_figures_audit_reject_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/public-figures/audit/figure/{pf_id}/verify-all":{"post":{"tags":["public-figures"],"summary":"Bulk-approve every pending handle for one figure","operationId":"public_figures_audit_verify_all_endpoint_api_public_figures_audit_figure__pf_id__verify_all_post","parameters":[{"name":"pf_id","in":"path","required":true,"schema":{"type":"string","title":"Pf Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/public-figures/audit/figure/{pf_id}/reject-all":{"post":{"tags":["public-figures"],"summary":"Bulk-reject every pending handle for one figure","operationId":"public_figures_audit_reject_all_endpoint_api_public_figures_audit_figure__pf_id__reject_all_post","parameters":[{"name":"pf_id","in":"path","required":true,"schema":{"type":"string","title":"Pf Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/alpha/entity/{entity_key}":{"get":{"tags":["alpha"],"summary":"Cached background for an entity named in an alpha find","operationId":"alpha_entity_endpoint_api_alpha_entity__entity_key__get","parameters":[{"name":"entity_key","in":"path","required":true,"schema":{"type":"string","title":"Entity Key"}},{"name":"kind","in":"query","required":false,"schema":{"type":"string","default":"","title":"Kind"}},{"name":"name","in":"query","required":false,"schema":{"type":"string","default":"","title":"Name"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/reader/stats":{"get":{"tags":["reader"],"summary":"Reader Stats Endpoint","description":"Admin: reader sign-ups by day/source, totals, recent readers.","operationId":"reader_stats_endpoint_api_reader_stats_get","parameters":[{"name":"days","in":"query","required":false,"schema":{"type":"integer","default":30,"title":"Days"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/review/queue":{"get":{"tags":["review"],"summary":"Review Queue Endpoint","operationId":"review_queue_endpoint_api_review_queue_get","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":20,"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/review/{review_id}/label":{"post":{"tags":["review"],"summary":"Review Label Endpoint","operationId":"review_label_endpoint_api_review__review_id__label_post","parameters":[{"name":"review_id","in":"path","required":true,"schema":{"type":"integer","title":"Review Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/alpha/publish/queue":{"get":{"tags":["alpha"],"summary":"Alpha posts awaiting review before going to the main account","operationId":"alpha_publish_queue_endpoint_api_alpha_publish_queue_get","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":50,"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/alpha/publish/{publication_id}/approve":{"post":{"tags":["alpha"],"summary":"Approve a queued post and send it to the main account","operationId":"alpha_publish_approve_endpoint_api_alpha_publish__publication_id__approve_post","parameters":[{"name":"publication_id","in":"path","required":true,"schema":{"type":"integer","title":"Publication Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/alpha/publish/{publication_id}/reject":{"post":{"tags":["alpha"],"summary":"Reject a queued post so it is never sent","operationId":"alpha_publish_reject_endpoint_api_alpha_publish__publication_id__reject_post","parameters":[{"name":"publication_id","in":"path","required":true,"schema":{"type":"integer","title":"Publication Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/agent/v1":{"get":{"tags":["agents"],"summary":"Agent service catalog","operationId":"agent_catalog_endpoint_api_agent_v1_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/agent/v1/quorums":{"get":{"tags":["agents"],"summary":"Quorum feed","operationId":"agent_quorums_endpoint_api_agent_v1_quorums_get","parameters":[{"name":"country","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Country"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":20,"title":"Limit"}},{"name":"cursor","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Cursor"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"402":{"description":"x402 payment required. The PAYMENT-REQUIRED header is the challenge."}},"x-payment-info":{"protocols":["x402"],"network":"eip155:8453","asset":"0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913","payTo":"0x9ce92953d33237af061c6b1aa29c597c8656b547","price":{"mode":"dynamic","currency":"USD","min":"0.01","max":"0.50"}}}},"/api/agent/v1/quorums/{quorum_id}":{"get":{"tags":["agents"],"summary":"One quorum","operationId":"agent_quorum_endpoint_api_agent_v1_quorums__quorum_id__get","parameters":[{"name":"quorum_id","in":"path","required":true,"schema":{"type":"string","title":"Quorum Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"402":{"description":"x402 payment required. The PAYMENT-REQUIRED header is the challenge."}},"x-payment-info":{"protocols":["x402"],"network":"eip155:8453","asset":"0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913","payTo":"0x9ce92953d33237af061c6b1aa29c597c8656b547","price":{"mode":"dynamic","currency":"USD","min":"0.01","max":"0.01"}}}},"/api/agent/v1/articles/{article_id}":{"get":{"tags":["agents"],"summary":"Article with its quorums","operationId":"agent_article_endpoint_api_agent_v1_articles__article_id__get","parameters":[{"name":"article_id","in":"path","required":true,"schema":{"type":"string","title":"Article Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"402":{"description":"x402 payment required. The PAYMENT-REQUIRED header is the challenge."}},"x-payment-info":{"protocols":["x402"],"network":"eip155:8453","asset":"0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913","payTo":"0x9ce92953d33237af061c6b1aa29c597c8656b547","price":{"mode":"dynamic","currency":"USD","min":"0.005","max":"0.005"}}}},"/api/agent/v1/events":{"get":{"tags":["agents"],"summary":"Alpha or news events","operationId":"agent_events_endpoint_api_agent_v1_events_get","parameters":[{"name":"stream","in":"query","required":false,"schema":{"type":"string","default":"alpha","title":"Stream"}},{"name":"country","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Country"}},{"name":"cell","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Cell"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":20,"title":"Limit"}},{"name":"cursor","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Cursor"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"402":{"description":"x402 payment required. The PAYMENT-REQUIRED header is the challenge."}},"x-payment-info":{"protocols":["x402"],"network":"eip155:8453","asset":"0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913","payTo":"0x9ce92953d33237af061c6b1aa29c597c8656b547","price":{"mode":"dynamic","currency":"USD","min":"0.005","max":"1.00"}}}}},"components":{"schemas":{"AlphaMatchRequest":{"properties":{"text":{"type":"string","title":"Text"},"top_k":{"type":"integer","title":"Top K","default":10}},"type":"object","required":["text"],"title":"AlphaMatchRequest","description":"Body for POST /api/alpha/match — semantic ranking of active alpha signals\nagainst a free-text query (e.g. a prediction-market question)."},"Body_contribute_geobit_endpoint_api_geobits_contribute_post":{"properties":{"evm_address":{"type":"string","title":"Evm Address"},"content_type":{"type":"string","title":"Content Type"},"latitude":{"type":"number","title":"Latitude"},"longitude":{"type":"number","title":"Longitude"},"captured_at":{"type":"string","title":"Captured At"},"text_content":{"type":"string","title":"Text Content","default":""},"evm_signature":{"type":"string","title":"Evm Signature","default":""},"location_attestation":{"type":"string","title":"Location Attestation","default":"{}"},"location_accuracy_meters":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Location Accuracy Meters"},"duration_seconds":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Duration Seconds"},"tags_json":{"type":"string","title":"Tags Json","default":"[]"},"device_fingerprint":{"type":"string","title":"Device Fingerprint","default":""},"source_url":{"type":"string","title":"Source Url","default":""},"platform":{"type":"string","title":"Platform","default":""},"file":{"anyOf":[{"type":"string","contentMediaType":"application/octet-stream"},{"type":"null"}],"title":"File"}},"type":"object","required":["evm_address","content_type","latitude","longitude","captured_at"],"title":"Body_contribute_geobit_endpoint_api_geobits_contribute_post"},"Body_file_dispute_endpoint_api_geobits_contribute__contribution_id__dispute_post":{"properties":{"evm_address":{"type":"string","title":"Evm Address"},"rationale":{"type":"string","title":"Rationale"}},"type":"object","required":["evm_address","rationale"],"title":"Body_file_dispute_endpoint_api_geobits_contribute__contribution_id__dispute_post"},"Body_reply_geobit_endpoint_api_geobits_reply_post":{"properties":{"evm_address":{"type":"string","title":"Evm Address"},"text_content":{"type":"string","title":"Text Content"},"reply_to_kind":{"type":"string","title":"Reply To Kind"},"reply_to_id":{"type":"string","title":"Reply To Id"},"device_fingerprint":{"type":"string","title":"Device Fingerprint","default":""}},"type":"object","required":["evm_address","text_content","reply_to_kind","reply_to_id"],"title":"Body_reply_geobit_endpoint_api_geobits_reply_post"},"Body_revise_contribution_endpoint_api_geobits_contribute__contribution_id__revise_post":{"properties":{"evm_address":{"type":"string","title":"Evm Address"},"text_content":{"type":"string","title":"Text Content","default":""},"file":{"anyOf":[{"type":"string","contentMediaType":"application/octet-stream"},{"type":"null"}],"title":"File"}},"type":"object","required":["evm_address"],"title":"Body_revise_contribution_endpoint_api_geobits_contribute__contribution_id__revise_post"},"Body_social_disconnect_endpoint_api_social_disconnect__platform__post":{"properties":{"evm_address":{"type":"string","title":"Evm Address"}},"type":"object","required":["evm_address"],"title":"Body_social_disconnect_endpoint_api_social_disconnect__platform__post"},"Body_social_reply_endpoint_api_social_reply_post":{"properties":{"evm_address":{"type":"string","title":"Evm Address"},"platform":{"type":"string","title":"Platform"},"text_content":{"type":"string","title":"Text Content"},"permalink":{"type":"string","title":"Permalink","default":""},"reply_to_kind":{"type":"string","title":"Reply To Kind","default":""},"reply_to_id":{"type":"string","title":"Reply To Id","default":""}},"type":"object","required":["evm_address","platform","text_content"],"title":"Body_social_reply_endpoint_api_social_reply_post"},"HTTPValidationError":{"properties":{"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"type":"array","title":"Detail"}},"type":"object","title":"HTTPValidationError"},"ValidationError":{"properties":{"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"type":"array","title":"Location"},"msg":{"type":"string","title":"Message"},"type":{"type":"string","title":"Error Type"},"input":{"title":"Input"},"ctx":{"type":"object","title":"Context"}},"type":"object","required":["loc","msg","type"],"title":"ValidationError"},"WebhookRegisterRequest":{"properties":{"url":{"type":"string","title":"Url"},"event_type":{"type":"string","title":"Event Type","default":"alpha.created"}},"type":"object","required":["url"],"title":"WebhookRegisterRequest","description":"Body for POST /api/alpha/webhooks — register an endpoint to receive new\nalpha signals."}},"securitySchemes":{"BearerApiKey":{"type":"http","scheme":"bearer","bearerFormat":"qz_pk_live_…","description":"Issued via `api seed-api-keys`. Pass as `Authorization: Bearer <key>` (or `X-Api-Key` for clients that can't set Authorization)."}}},"tags":[{"name":"feed","description":"Geobit feed access — the raw chronological stream of geo-located posts, optionally scoped to a country, a named region, or the global feed.\n\n**`/api/feeds/{scope}`** is the recommended path for agents and SSR clients — it serves a pre-baked GCS snapshot with sub-second latency and can be edge-cached. **`/api/geobits/feed*`** are live-queried, use only when you need stricter freshness than the snapshot (≤5 min) or per-source filtering."},{"name":"quorum","description":"LLM-clustered topic + opinion digests over recent geobits in a geographic scope. Use these when you want \"what is happening here\" rather than \"give me every post.\"\n\nCountry and region runs refresh on a tier schedule (hot countries every 15–30 min, baseline 1–2 h, rest-of-world ≤12 h). The `/area` endpoint runs an LLM call on-demand for arbitrary bboxes (≤60 s on cache miss)."},{"name":"tags","description":"Tag autocomplete over the corpus's user-contributed + classifier-extracted tag set. Tags are populated by the geobit classifier and by iWitness contributors."},{"name":"media","description":"Content-addressed access to the photo/video bytes referenced by a geobit's `preview_url`. Bytes are immutable once stored — safe to cache forever. **This route is unauthenticated** (the pointer itself is the capability) so `<img src=…>` from a browser works."},{"name":"layers","description":"Map-layer payloads composed by backend workers: live flight states (OpenSky relay), ships, webcams, earthquakes (USGS), wildfires (NASA), severe-weather alerts (NOAA), and ACLED conflict events. Each snapshot is refreshed every few minutes; the `/bootstrap-snapshot` endpoint composes them all in one response."},{"name":"public-figures","description":"Heads of state, ministers, parliamentarians, judges, central-bank governors, CEOs, and other consequential voices — what they have said publicly (via their verified social-media handles + RSS-fed press statements), how it clusters into topics + stances, and **when they flip stance** (\"shift\").\n\nEach figure carries a roster of `topics` (a topic = a subject the figure has repeatedly addressed) and per-topic `opinions` (the distinct stances they have taken). When the figure publicly contradicts a prior stance — switching from one opinion to another within a topic — we flag a **stance shift** with a short human rationale, the from/to opinion ids, and the flip age in days.\n\nThe full bundle is pre-baked every 120 s; use `/markers` for the lightweight map glyph index, `/all` for the full drill-down tree, `/figure/{pf_id}` for a deep-link, and `/shifts` for a focused list of recent stance shifts across all figures."},{"name":"stats","description":"Public ingestion-monitor stats — per-platform / per-country geobit throughput, classifier rejection rates, source-sampling SLO. These power the public dashboards at quorumz.com/stats and grafana.quorumz.com."},{"name":"auth","description":"User account routes for the iWitness flow — register a username, fetch the encrypted-keys recovery bundle, look up an account by EVM address. WebAuthn/passkey ceremony is browser-side; this API stores only the encrypted keys + alias mapping. **These routes are exempt from API-key auth** since they are signed by the user's passkey."},{"name":"contribute","description":"iWitness submission + confirmation routes. The submitter's EVM signature over a canonical payload is the auth — these routes are exempt from API-key auth. Confirmations + disputes are also signed."},{"name":"system","description":"Health checks. No auth."}],"security":[{"BearerApiKey":[]}],"x-discovery":{"ownershipProofs":["0x9ce92953d33237af061c6b1aa29c597c8656b547"],"docs":"https://sense.quorumz.com/agents/docs"}}