{"openapi":"3.1.0","info":{"title":"Mantidae v3.0","version":"0.1.0"},"paths":{"/webhooks/stripe":{"post":{"summary":"Stripe Webhook","description":"Stripe subscription lifecycle → users-table tenant lifecycle (§7).\n\nVerified by the Stripe-Signature HMAC over the RAW body (NOT the parsed\nJSON — re-serializing would change the bytes and break verification).\nReturns 200 for handled AND intentionally-ignored events so Stripe stops\nretrying; 400 only on a signature/parse failure; 503 if unconfigured.","operationId":"stripe_webhook_webhooks_stripe_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/.well-known/oauth-protected-resource":{"get":{"summary":"Oauth Protected Resource","operationId":"oauth_protected_resource__well_known_oauth_protected_resource_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/.well-known/mcp/server-card.json":{"get":{"summary":"Mcp Server Card","description":"Server info, how to authenticate, and the tool list, for MCP directories\nwhose scanners cannot sign in (Smithery reads this instead of scanning).\nPublic on purpose: it is the same tool list tools/list returns.","operationId":"mcp_server_card__well_known_mcp_server_card_json_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/.well-known/oauth-authorization-server":{"get":{"summary":"Oauth Metadata","operationId":"oauth_metadata__well_known_oauth_authorization_server_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/oauth/register":{"post":{"summary":"Oauth Register","operationId":"oauth_register_oauth_register_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/oauth/authorize":{"get":{"summary":"Oauth Authorize","operationId":"oauth_authorize_oauth_authorize_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}},"post":{"summary":"Oauth Authorize Submit","operationId":"oauth_authorize_submit_oauth_authorize_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/oauth/token":{"post":{"summary":"Oauth Token","operationId":"oauth_token_oauth_token_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/health":{"get":{"summary":"Health","description":"Public shallow health; detailed product/client info requires auth (v3.6 C8).","operationId":"health_health_get","parameters":[{"name":"authorization","in":"header","required":false,"schema":{"type":"string","title":"Authorization"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/products/reload":{"post":{"summary":"Reload Products","description":"Hot-reload product cache after DB changes without restarting the server.\nBuilds the new cache first, then swaps atomically so concurrent scout runs\nnever observe an empty cache window (BUG-09).\n\n2026-09-24: a tenant reloads only its OWN products. Any key used to rebuild\nthe whole cache — re-embedding every tenant's anchors on the brain's OpenAI\nkey, as often as a customer cared to call it.","operationId":"reload_products_products_reload_post","parameters":[{"name":"authorization","in":"header","required":false,"schema":{"type":"string","title":"Authorization"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/products/list":{"get":{"summary":"List Products","operationId":"list_products_products_list_get","parameters":[{"name":"authorization","in":"header","required":false,"schema":{"type":"string","title":"Authorization"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/products/update":{"post":{"summary":"Update Product","description":"Edit a product you own, or pause and resume it. Only the fields you send change.\n\nBody: product_id plus any of name, description, value_prop, target_audience,\nanchor_sentences (same rules as add_product), buy_signal_keywords,\ncompetitor_keywords, active. active=false pauses the product: the scout stops\nreading its stations and /signal/score no longer accepts its id until it is\nresumed. Products are paused rather than deleted, because leads, missions and\nprospects keep their history.","operationId":"update_product_products_update_post","parameters":[{"name":"authorization","in":"header","required":false,"schema":{"type":"string","title":"Authorization"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Payload"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/products/add":{"post":{"summary":"Add Product","description":"Register a new product for lead monitoring and load it into the cache.","operationId":"add_product_products_add_post","parameters":[{"name":"authorization","in":"header","required":false,"schema":{"type":"string","title":"Authorization"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Payload"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/stations/add":{"post":{"summary":"Add Station","operationId":"add_station_stations_add_post","parameters":[{"name":"authorization","in":"header","required":false,"schema":{"type":"string","title":"Authorization"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Payload"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/stations/update":{"post":{"summary":"Update Station","description":"Pause, resume, rename or re-point a station you own.\n\nBody: station_id plus any of active, name, rss_url, keyword. A new rss_url\nis validated like add_station's.","operationId":"update_station_stations_update_post","parameters":[{"name":"authorization","in":"header","required":false,"schema":{"type":"string","title":"Authorization"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Payload"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/stations/remove":{"post":{"summary":"Remove Station","description":"Remove a station you own.\n\nA station that never produced a lead is deleted. One whose feed already\nproduced leads keeps that history: it is switched off instead, and the reply\nsays so (status \"paused\", reason \"has_history\").","operationId":"remove_station_stations_remove_post","parameters":[{"name":"authorization","in":"header","required":false,"schema":{"type":"string","title":"Authorization"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Payload"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/stations/list":{"get":{"summary":"List Stations","description":"The caller's stations, each with a health readout over the last N days.\n\nQuery: ?product_id=<uuid> to scope to one product (optional).\n\nPer station:\n  leads_captured   posts the feed actually yielded\n  surfaced         how many became missions you could act on\n  empty_fetches    fetches that returned nothing\n  rate_limited     of those, how many were the source throttling US\n  weight           favoured | neutral | demoted (learned from your feedback)\n  verdict          plain-language reading — see the module note above","operationId":"list_stations_stations_list_get","parameters":[{"name":"product_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Product Id"}},{"name":"authorization","in":"header","required":false,"schema":{"type":"string","title":"Authorization"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/scout/ingest":{"post":{"summary":"Scout Ingest","description":"Score a feed page the operator's own client read. Tenant-scoped.\n\nBody: {\"station_id\": \"...\", \"entries\": [{\"link\", \"title\", \"summary\",\n\"author\", \"published\"}, ...]} with 1-50 entries. The station must belong to\nthe caller and be active. Scoring runs in the background like a scout\ncycle; missions appear in the queue and on the stream as usual.","operationId":"scout_ingest_scout_ingest_post","parameters":[{"name":"authorization","in":"header","required":false,"schema":{"type":"string","title":"Authorization"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Payload"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/stations/preview":{"post":{"summary":"Preview Station Endpoint","description":"Judge a candidate feed for a product BEFORE adding it as a station. Writes nothing.\n\nBody: {\"product_id\", \"rss_url\"} to have the brain fetch the feed, or\n{\"product_id\", \"entries\": [...]} with up to 50 entries your own client read\n(same shape as /scout/ingest; required for feeds marked read_by: client).\nOptional \"sample\": how many best-matching posts the panel judges (1-12,\ndefault 8; one judgement each).","operationId":"preview_station_endpoint_stations_preview_post","parameters":[{"name":"authorization","in":"header","required":false,"schema":{"type":"string","title":"Authorization"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Payload"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/scout/launch_batch":{"post":{"summary":"Launch Batch Scout","description":"Cron heartbeat endpoint. Fires one background task per active product.\n\nA tenant key (the plugin's scout_now) gets a scan of its own products only.","operationId":"launch_batch_scout_scout_launch_batch_post","parameters":[{"name":"authorization","in":"header","required":false,"schema":{"type":"string","title":"Authorization"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/sync/missions":{"get":{"summary":"Sync Missions","description":"Sidecar polls this to get missions that need drafting or executing.\n\n?since=<ISO timestamp>     — only return missions updated after this time\n?limit=<int>               — max rows returned (default 200, max 200)\n?include=draft_context     — attach draft_context payload for draft_needed missions\n?sort=newest               — newest leads first (default for operator dashboard)\n?sort=consensus            — lowest swarm disagreement first (S3.1 original behaviour)","operationId":"sync_missions_sync_missions_get","parameters":[{"name":"since","in":"query","required":false,"schema":{"type":"string","title":"Since"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":200,"title":"Limit"}},{"name":"include","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Include"}},{"name":"sort","in":"query","required":false,"schema":{"type":"string","default":"newest","title":"Sort"}},{"name":"authorization","in":"header","required":false,"schema":{"type":"string","title":"Authorization"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/actions/upload_draft":{"post":{"summary":"Upload Draft","description":"Upload an LLM-drafted message and (optionally) swarm judge metadata.\n\nPayload:\n  id             : mission uuid\n  content        : drafted message body\n  disagreement   : float [0..1] — max-min across judge scores (S0.3)\n  skeptic        : float [0..1]\n  analyst        : float [0..1]\n  optimist       : float [0..1]\n  low_confidence : bool — true when swarm fused < 0.40 and `content` is\n                   a placeholder asking the operator to write the draft\n                   manually. Dashboard collapses these by default.","operationId":"upload_draft_actions_upload_draft_post","parameters":[{"name":"authorization","in":"header","required":false,"schema":{"type":"string","title":"Authorization"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Payload"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/actions/ack":{"post":{"summary":"Ack Mission","description":"Legacy send-ack (MCP + the Railway sidecar). v4 senders use\n/v4/missions/{id}/ack instead; both delegate to _apply_mission_ack so the\nstatus-flip + v3.7.10 audit-trail behavior is identical through either door.","operationId":"ack_mission_actions_ack_post","parameters":[{"name":"authorization","in":"header","required":false,"schema":{"type":"string","title":"Authorization"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Payload"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v4/me":{"get":{"summary":"V4 Me","description":"Self-serve account lookup for the dashboard (v4.0 W5, §3.3 onboarding).\n\nAUTH IS DELIBERATELY DIFFERENT from every other /v4/* route. This is the ONE\nendpoint a user hits BEFORE they possess their operator key (it is how they\nretrieve it), so it CANNOT use _auth() (which resolves the operator key). It\ninstead authenticates the caller's Supabase Auth session: the dashboard\nsends the user's Supabase access token as the Bearer, we validate it against\nSupabase (gotrue), take the VERIFIED email off the session, and return that\ntenant's row.\n\nReturns ONLY the caller's own account metadata — operator key, subscription\nstatus, auto-fire threshold. It never touches leads/missions/prospects, so\nno scoring/IP is exposed and no RLS is required: the pipeline stays agentic\n(the MCP agent is the pipeline UI), this is purely the key-delivery +\naccount surface.\n\n404 (not 401) when the Supabase email has no users row yet — i.e. the person\nauthenticated a magic link but never completed Stripe checkout. The dashboard\nshows a \"subscribe to get your key\" state on a 404.","operationId":"v4_me_v4_me_get","parameters":[{"name":"authorization","in":"header","required":false,"schema":{"type":"string","title":"Authorization"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v4/me/key":{"post":{"summary":"V4 Me Key","description":"Create or replace the caller's operator key, and return it ONCE.\n\nSession-authenticated exactly like /v4/me (the caller may have no key yet).\nStores only the SHA-256 and a 13-char prefix; the previous key, if any,\nstops working immediately (no grace period — operator decision 2026-09-22).","operationId":"v4_me_key_v4_me_key_post","parameters":[{"name":"authorization","in":"header","required":false,"schema":{"type":"string","title":"Authorization"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v4/threshold/set":{"post":{"summary":"V4 Set Threshold","description":"Set the authenticated tenant's auto-fire threshold (§3.3).\n\nBody: { \"auto_threshold\": 0.75 }. Range [0.0, 1.01]; 1.01 = manual (nothing\nauto-fires — the default), ~0.75 balanced, ~0.55 aggressive. Compared against\nthe 0–1 swarm fused score by the #23 SSE gate. Stored on the users row\n(numeric(3,2)); the auth cache is invalidated so the new value is live now\nrather than up to one TTL later.","operationId":"v4_set_threshold_v4_threshold_set_post","parameters":[{"name":"authorization","in":"header","required":false,"schema":{"type":"string","title":"Authorization"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Payload"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v4/quiet_hours":{"get":{"summary":"V4 Get Quiet Hours","description":"The tenant's quiet hours: {\"quiet_hours\": null} (the default: the scout\nruns around the clock) or {\"quiet_hours\": {\"timezone\", \"start\", \"end\"}}.","operationId":"v4_get_quiet_hours_v4_quiet_hours_get","parameters":[{"name":"authorization","in":"header","required":false,"schema":{"type":"string","title":"Authorization"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"summary":"V4 Set Quiet Hours","description":"Set or clear the hours the scout skips your products (QUIET HOURS, PER TENANT).\n\nBody: {\"timezone\": \"America/New_York\", \"start\": \"23:00\", \"end\": \"07:00\"}\n(24-hour HH:MM, may cross midnight), or {\"off\": true} to scout around the\nclock. Client-side reading (/scout/ingest) is never paused: your own machine\ndecides when to read.","operationId":"v4_set_quiet_hours_v4_quiet_hours_post","parameters":[{"name":"authorization","in":"header","required":false,"schema":{"type":"string","title":"Authorization"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Payload"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v4/sender/status":{"get":{"summary":"V4 Sender Status","description":"Sender health/registration poll (§3.3).\n\nA non-401 response confirms the operator key is valid + active. Returns the\nopen-mission queue depth (work the sender still has to act on), mirroring the\n/sync/missions status set, plus the tenant's current auto_threshold so the\nsender knows the gate without a second call. `version` is null until the\nsender reports its own (W3/W4); _auth already bumped last_seen_at.","operationId":"v4_sender_status_v4_sender_status_get","parameters":[{"name":"authorization","in":"header","required":false,"schema":{"type":"string","title":"Authorization"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v4/missions/{mission_id}/ack":{"post":{"summary":"V4 Ack Mission","description":"v4 sender send-ack (§3.3) — replaces /actions/ack for daemon/plugin senders.\n\nPath id + body { \"outcome\": \"success\"|\"failed\", \"error_class\": \"...\",\n\"platform_response\": \"...\" }. Shares _apply_mission_ack with the legacy\nendpoint, then adds sender-channel health signalling for the failure classes\nthe platforms hand back.","operationId":"v4_ack_mission_v4_missions__mission_id__ack_post","parameters":[{"name":"mission_id","in":"path","required":true,"schema":{"type":"string","title":"Mission Id"}},{"name":"authorization","in":"header","required":false,"schema":{"type":"string","title":"Authorization"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Payload"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v4/missions/stream":{"get":{"summary":"V4 Missions Stream","description":"SSE stream of send-ready missions for the authenticated tenant (§3.3, #23).\n\nThe v4 replacement for the sidecar's \"poll /sync/missions for\nstatus='approved' -> execute\" loop: a user-side sender (daemon #25 / plugin\nv2.0 #26) holds this stream open, sends each `mission_ready`, then acks via\n/v4/missions/{id}/ack.\n\nNamed text/event-stream events:\n  connected     once on open; carries the tenant's auto_threshold.\n  mission_ready a mission to send (lean, send-only projection).\n  heartbeat     every ~30s; keep-alive + reports pause (backoff) state.\n  shutdown      server is stopping, or the token is no longer valid -> close.\n\nTHE AUTO-FIRE GATE LIVES HERE (the §3.3 SCOPE NOTE): a mission auto-fires\nonly when its 0-1 fused_score >= users.auto_threshold (default 1.01 =\nmanual). Enforced in _sse_poll (promotes pending -> approved). DELIVERY IS\nAT-LEAST-ONCE: a reconnect re-emits any approved-but-unacked mission, so the\nsender MUST dedupe per mission id (an ack flips status -> 'sent', which drops\nit from the stream). Exactly-once (an emit-time 'dispatched' status + a\ntimeout re-drive) is a deliberate follow-up, out of scope for the\nsingle-sender-per-tenant MVP.","operationId":"v4_missions_stream_v4_missions_stream_get","parameters":[{"name":"authorization","in":"header","required":false,"schema":{"type":"string","title":"Authorization"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/missions/failed":{"get":{"summary":"List Failed Missions","description":"Return missions in 'failed' status with retry_count < 3, newest first.\nIncludes lead title, URL, snippet, and signal_score so the operator can\nsee what was found and why it was suppressed.","operationId":"list_failed_missions_missions_failed_get","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":100,"title":"Limit"}},{"name":"authorization","in":"header","required":false,"schema":{"type":"string","title":"Authorization"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/missions/retry_failed":{"post":{"summary":"Retry Failed Missions","description":"Resurrect failed missions (retry_count < 3) back to 'approved' so the\nsidecar picks them up again. Increments retry_count. Intended to be\ncalled by a daily Supabase pg_cron job.","operationId":"retry_failed_missions_missions_retry_failed_post","parameters":[{"name":"authorization","in":"header","required":false,"schema":{"type":"string","title":"Authorization"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/actions/approve":{"post":{"summary":"Approve Mission","description":"Approve a mission (optionally with an edited draft).\n\nApproval fires the `replied` RL nudge inline via\n`_record_outcome_once` — symmetric with /actions/reject, which has\nalways fired RL inline. The helper is idempotent, so the v1.5.x\nplugin's existing second /feedback/record call doesn't double-fire.\n\nCalled by the SignalPipe OpenClaw plugin and any direct integrations.","operationId":"approve_mission_actions_approve_post","parameters":[{"name":"authorization","in":"header","required":false,"schema":{"type":"string","title":"Authorization"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Payload"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/actions/mark_sent":{"post":{"summary":"Mark Sent","description":"Operator marker: \"I sent this mission by hand.\"\n\nThe gap this closes (the Cosankov case, 2026-05-19): when a mission's\noutreach_channel is 'manual' — or whenever the operator sends the DM /\npublic reply themselves via the platform UI instead of letting the\nsidecar/daemon send it — nothing tells the brain the send happened. The\nmission sits in 'approved'/'draft_needed' forever, no prospect is born,\nno message_sent interaction is logged, and a later get_message_prompt\nfollow-up draft is blind to what we already said.\n\nThis is the operator-side equivalent of a successful /actions/ack:\n  1. flips the mission to 'sent',\n  2. fires the same positive 'replied' RL nudge as /actions/approve —\n     a manual send is an endorsement, and under the temperature-gated\n     DM model (see project_signalpipe_v4_plan.md §3.6) manual / public\n     first-touch is the COMMON path, so its RL signal must not be lost,\n  3. records the outbound message (prospect upsert at temperature=10 /\n     mode=nurture + message_sent interaction + last_signal).\n\nAll three helpers are idempotent, so the operator calling /actions/approve\nfirst and /actions/mark_sent second does NOT double-fire RL or duplicate\nthe interaction row.\n\nBody: { \"id\": \"<mission_id>\" }","operationId":"mark_sent_actions_mark_sent_post","parameters":[{"name":"authorization","in":"header","required":false,"schema":{"type":"string","title":"Authorization"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Payload"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/actions/reject":{"post":{"summary":"Reject Mission","description":"Reject a mission and update its status in the database.\nOptionally accepts a rejection_reason for smarter RL learning.\n\nValid reasons: 'not_relevant', 'sarcasm', 'wrong_product', 'spam',\n               'too_vague', 'already_customer', 'no_reason'","operationId":"reject_mission_actions_reject_post","parameters":[{"name":"authorization","in":"header","required":false,"schema":{"type":"string","title":"Authorization"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Payload"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/actions/mission/{mission_id}":{"delete":{"summary":"Delete Mission","description":"Hard-delete a mission row. No RL signal — use for noise/duplicates.\n\nv3.7.17 — when the sidecar's swarm-suppress path calls this with\n`?reason=swarm_suppressed&fused=..&skeptic=..&analyst=..&optimist=..`,\nwe first emit a `mission_suppressed` event capturing the scout's\nraw_score alongside the swarm S/A/O scores BEFORE deleting. This closes\nthe observability blind spot that hid the ~2-day, ~100% suppression\nincident (drought began 2026-05-28 09:20:35): missions were created,\ndeleted, and left no trace, so the queue going dry looked like an empty\nfeed rather than mass over-veto. The raw_score vs fused-score gap in\neach event is the Case-A (correct junk kill) vs Case-B (buyer wrongly\nvetoed) discriminator — a high scout raw_score with a pinned fused~0.20\nflags a buyer we shouldn't have dropped. Manual MCP/UI deletes pass no\n`reason`, so their behavior is unchanged (no event, no extra read).","operationId":"delete_mission_actions_mission__mission_id__delete","parameters":[{"name":"mission_id","in":"path","required":true,"schema":{"type":"string","title":"Mission Id"}},{"name":"reason","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Reason"}},{"name":"fused","in":"query","required":false,"schema":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Fused"}},{"name":"skeptic","in":"query","required":false,"schema":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Skeptic"}},{"name":"analyst","in":"query","required":false,"schema":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Analyst"}},{"name":"optimist","in":"query","required":false,"schema":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Optimist"}},{"name":"authorization","in":"header","required":false,"schema":{"type":"string","title":"Authorization"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/companion/track":{"post":{"summary":"Companion Track","description":"Log a prospect interaction signal. Creates the prospect if new.\nUpdates temperature with momentum and time-decay. Returns new state + follow-up timing.","operationId":"companion_track_companion_track_post","parameters":[{"name":"authorization","in":"header","required":false,"schema":{"type":"string","title":"Authorization"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Payload"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/companion/message":{"post":{"summary":"Companion Message","description":"Generate the next message for a prospect using their full state,\nproduct context, conversation history, and objection memory.\n\n?mode=generated (default) — backend calls the LLM and returns the message.\n?mode=prompt              — backend returns the system prompt + user_content\n                             + response schema, WITHOUT calling the LLM. The\n                             client (OpenClaw plugin / remote MCP) runs its\n                             own LLM, then posts the result back with\n                             `mode=record`. This shifts LLM cost off the\n                             backend and onto the user's subscription.\n?mode=record              — client-generated message is logged as an\n                             interaction. Requires `message` in payload.\n                             Daily cap still enforced.","operationId":"companion_message_companion_message_post","parameters":[{"name":"mode","in":"query","required":false,"schema":{"type":"string","default":"generated","title":"Mode"}},{"name":"authorization","in":"header","required":false,"schema":{"type":"string","title":"Authorization"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Payload"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/companion/record_reply":{"post":{"summary":"Companion Record Reply","description":"Capture a prospect's reply text after we sent them a message.\nLogs to prospect_interactions.reply_text + updates prospects.last_reply_text,\nthen runs an LLM intent/sentiment pass to update temperature, mode, and objections.\n\npayload: {prospect_id, reply_text, channel?: \"twitter\"|\"reddit\"|\"email\"}","operationId":"companion_record_reply_companion_record_reply_post","parameters":[{"name":"authorization","in":"header","required":false,"schema":{"type":"string","title":"Authorization"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Payload"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/companion/pipeline":{"get":{"summary":"Companion Pipeline","description":"All prospects sorted hottest first. Includes summary counts per mode.","operationId":"companion_pipeline_companion_pipeline_get","parameters":[{"name":"authorization","in":"header","required":false,"schema":{"type":"string","title":"Authorization"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/analytics/refresh_send_windows":{"post":{"summary":"Refresh Send Windows","description":"S3.4 — Rebuild send_windows cache from prospect_interactions.\nFor each (product_id, platform, dow, hour) bucket, compute reply_rate =\nreplied / sent. Intended to be called nightly via pg_cron.","operationId":"refresh_send_windows_analytics_refresh_send_windows_post","parameters":[{"name":"authorization","in":"header","required":false,"schema":{"type":"string","title":"Authorization"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/products/suggest_anchors":{"post":{"summary":"Suggest Anchors","description":"Generate suggested anchor sentences for a product using its name + value_prop +\ntarget_audience as seed context. Used during onboarding so users don't have to\nwrite 5-10 anchors from scratch.\n\npayload: {name, value_prop, target_audience, description?, count?: int (default 8)}\nReturns:  {anchors: [str, ...]}","operationId":"suggest_anchors_products_suggest_anchors_post","parameters":[{"name":"authorization","in":"header","required":false,"schema":{"type":"string","title":"Authorization"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Payload"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/products/refresh_anchors":{"post":{"summary":"Refresh Anchors","operationId":"refresh_anchors_products_refresh_anchors_post","parameters":[{"name":"authorization","in":"header","required":false,"schema":{"type":"string","title":"Authorization"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Payload"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/feedback/record":{"post":{"summary":"Record Feedback","description":"RL weight update. Called by the plugin after approve/reject and by\nany direct integration that wants to record an outcome.\n\nAs of v3.7.7 the RL weight lives on the SOURCE STATION, not the\nproduct. This endpoint resolves mission → lead → station_id and\nadjusts stations.rl_weight:\n  replied  → +0.05 (clamped to 2.0)\n  ignored  → −0.02 (clamped to 0.5)\n\nLegacy leads with station_id = NULL skip the rl_weight delta but\nstill record into products.rejection_stats for analytics.\n\nIdempotent (v3.7.7): re-recording an outcome on a mission that\nalready has one is a silent no-op. /actions/approve now fires\n`replied` inline on every approval, so when the v1.5.x plugin calls\n/feedback/record as a follow-up, this endpoint sees the outcome\nalready set and returns the existing weight unchanged.","operationId":"record_feedback_feedback_record_post","parameters":[{"name":"authorization","in":"header","required":false,"schema":{"type":"string","title":"Authorization"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Payload"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/signal/score":{"post":{"summary":"Score Signal Endpoint","description":"Score arbitrary text against a product profile.\n\nBody:\n    text         (required)  — content to score\n    product_id   (required)  — product profile to score against\n    source_hint  (optional)  — channel label (\"gmail\", \"slack\", \"discord\",\n                                \"telegram\", \"linkedin\", \"whatsapp\", etc.)\n                                Used for drafting-tone hints only; we do\n                                not connect to the source.\n    context      (optional)  — the conversation the text belongs to: the\n                                post a comment replies to, the thread above\n                                a Slack message, the email being answered.\n                                Up to 2,000 characters. Shown to the judges\n                                as background; the score is still about the\n                                TEXT's author.\n\nReturns score, role, classification, sub-scores, competitor info,\npanel_verdict, and a drafting_context block when the score is high enough\nto act on.","operationId":"score_signal_endpoint_signal_score_post","parameters":[{"name":"authorization","in":"header","required":false,"schema":{"type":"string","title":"Authorization"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Payload"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/signal/score_batch":{"post":{"summary":"Score Signal Batch Endpoint","description":"Score up to SIGNAL_SCORE_BATCH_MAX texts against one product profile.\n\nBody:\n    product_id  (required) — one profile for the whole batch\n    items       (required) — list of {id?, text, source_hint?, context?}, 1..MAX.\n                             `context` is the post or thread the text\n                             replies to (see /signal/score).\n                             `id` is echoed back untouched so you can line\n                             results up with your own records; we neither\n                             store nor interpret it.\n    include_drafting_context (optional, default true) — set false for a\n                             pure triage pass. Drafting context costs a\n                             database read per qualifying item, and a\n                             caller sorting a scrape usually wants the\n                             verdicts first and the drafts later.\n\nReturns one result per item IN THE ORDER SUPPLIED, plus a summary. A bad\nitem fails on its own — it does not take the batch down with it, because\nlosing 24 good verdicts to one malformed row is not an acceptable trade\nfor a caller who is feeding us machine-generated input.\n\n`judgements_spent` is the billing unit: panels actually convened. Text\ndecided on content alone spends nothing, so this is normally well below\nthe item count.","operationId":"score_signal_batch_endpoint_signal_score_batch_post","parameters":[{"name":"authorization","in":"header","required":false,"schema":{"type":"string","title":"Authorization"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Payload"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/demo/score":{"post":{"summary":"Demo Score","description":"PUBLIC, unauthenticated swarm demo. Verdict + per-judge stance only —\nno numbers, no draft. See the moat-boundary note above.","operationId":"demo_score_demo_score_post","requestBody":{"content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Payload"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/demo/quiz":{"post":{"summary":"Demo Quiz","description":"PUBLIC, unauthenticated. Records one completed \"can you tell?\" run.\n\nBody: {\"set\": \"<item-set version>\", \"answers\": [{\"i\": 0, \"a\": true}, ...]}\n\nStores the ANSWERS, never a correctness count. The answer key lives with\nthe item set, so if a label is ever corrected the historical runs can be\nre-scored against the corrected key instead of being stranded.","operationId":"demo_quiz_demo_quiz_post","requestBody":{"content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Payload"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/analytics/scoring":{"get":{"summary":"Analytics Scoring","description":"Per-product scoring analytics: RL weight, rejection breakdown, hit rates.","operationId":"analytics_scoring_analytics_scoring_get","parameters":[{"name":"authorization","in":"header","required":false,"schema":{"type":"string","title":"Authorization"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/analytics/funnel":{"get":{"summary":"Analytics Funnel","operationId":"analytics_funnel_analytics_funnel_get","parameters":[{"name":"authorization","in":"header","required":false,"schema":{"type":"string","title":"Authorization"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/stats/public":{"get":{"summary":"Stats Public","description":"Returns the most recent public_stats row plus a 7-day rollup.\nNo auth. Safe for marketing-site embedding.","operationId":"stats_public_stats_public_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/stats/refresh_public":{"post":{"summary":"Refresh Public Stats","description":"Aggregates the past 24h into a new public_stats row. Intended to be\ncalled by a nightly pg_cron job.","operationId":"refresh_public_stats_stats_refresh_public_post","parameters":[{"name":"authorization","in":"header","required":false,"schema":{"type":"string","title":"Authorization"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/ops/daily_check":{"post":{"summary":"Daily Check Now","description":"Today's daily check, as {subject, body, needs}. Sends nothing.\n\nThe report names customers, so only the operator, the cron, and the\nDAILY_CHECK_KEY the mailing workflow holds may read it.","operationId":"daily_check_now_ops_daily_check_post","parameters":[{"name":"authorization","in":"header","required":false,"schema":{"type":"string","title":"Authorization"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}}},"components":{"schemas":{"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"}}}}