{"openapi":"3.0.3","info":{"title":"Valcour Ad-Targeting API","version":"0.1.0","description":"Early-access API behind the ad-targeting assistant: rank where to advertise by opportunity, and talk to the assistant directly. Endpoints and shapes may change while the product is in early access."},"paths":{"/api/v1/ai/ad-targeting/opportunity":{"post":{"summary":"Rank geographies by ad-targeting opportunity.","description":"Scores geographies as reach × favorability × audience-fit ÷ (1 + ad-saturation), built from public data sources (Census ACS, Meta Ad Library, Google Political Ads, FEC). Reach (audience size) is a first-class term so large markets are not zeroed out by a low percentage. Fail-soft per source: any source returning nothing is listed in data_sources_unavailable, and a best-effort ranking is still produced.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OpportunityRequest"}}}},"responses":{"200":{"description":"Ranked geographies with scores, confidence bands, and source provenance.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OpportunityResponse"}}}},"401":{"description":"unauthorized — missing or invalid bearer token"},"422":{"description":"invalid request body"},"429":{"description":"rate limited"}}}},"/api/assistant/chat":{"post":{"summary":"Ask the targeting assistant a question (streaming).","description":"Same-origin proxy to the assistant. Send a message; the response is a Server-Sent Events (SSE) stream of the assistant's reply, including any tool runs (such as the opportunity ranking). This is the endpoint the public /chat page uses. Unauthenticated and rate-limited per IP.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChatRequest"}}}},"responses":{"200":{"description":"text/event-stream of assistant events (text deltas, tool_start, tool_result)."},"400":{"description":"invalid input"},"429":{"description":"rate limited"},"503":{"description":"assistant temporarily unavailable"}}}},"/api/health":{"get":{"summary":"DB-deep liveness probe.","responses":{"200":{"description":"Service healthy — database reachable."},"503":{"description":"Service unhealthy — database unreachable."}}}}},"components":{"schemas":{"OpportunityRequest":{"type":"object","properties":{"state":{"type":"string","nullable":true,"description":"Optional state name to restrict the ranking to (e.g. 'Pennsylvania')."},"race":{"type":"string","nullable":true,"description":"Optional race label for context (e.g. '2026 PA Senate')."},"candidates":{"type":"array","nullable":true,"items":{"type":"string"},"description":"Optional candidate names used for ad-spend lookups."},"limit":{"type":"integer","minimum":1,"maximum":500,"description":"Max number of ranked geographies to return (default 50)."}}},"OpportunityResponse":{"type":"object","required":["results","competitiveness_source","data_sources_used","data_sources_unavailable","notes"],"properties":{"race":{"type":"string","nullable":true},"state":{"type":"string","nullable":true},"results":{"type":"array","items":{"$ref":"#/components/schemas/OpportunityItem"}},"competitiveness_source":{"type":"string","description":"How competitiveness was derived (e.g. a historical-lean proxy)."},"data_sources_used":{"type":"array","items":{"type":"string"},"description":"Data sources that contributed to this ranking."},"data_sources_unavailable":{"type":"array","items":{"type":"string"},"description":"Data sources that returned nothing for this request."},"notes":{"type":"array","items":{"type":"string"},"description":"Plain-language caveats about how the ranking was produced."}}},"OpportunityItem":{"type":"object","required":["geography","opportunity_score","ci_low","ci_high","competitiveness","persuadable_density","ad_saturation","why","components"],"properties":{"geography":{"type":"string","description":"The geography being scored (e.g. a county)."},"opportunity_score":{"type":"number","description":"Point opportunity score."},"ci_low":{"type":"number","description":"5th-percentile bound of the confidence band."},"ci_high":{"type":"number","description":"95th-percentile bound of the confidence band."},"competitiveness":{"type":"number","minimum":0,"maximum":1},"persuadable_density":{"type":"number","minimum":0,"maximum":1},"ad_saturation":{"type":"number","minimum":0,"maximum":1},"why":{"type":"string","description":"Short explanation of why this geography ranked where it did."},"components":{"type":"object","description":"The underlying component values that produced the score."}}},"ChatRequest":{"type":"object","required":["message"],"properties":{"message":{"type":"string","minLength":1,"maxLength":8000,"description":"The user's question for the assistant."},"conversation_id":{"type":"string","nullable":true,"description":"Optional id to continue an existing conversation."},"page_context":{"type":"object","description":"Optional context about the page the user is on."}}}},"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer"}}}}