{
  "openapi": "3.1.0",
  "info": {
    "title": "Minara Agent Gateway",
    "version": "0.0.0",
    "description": "REST + SSE surface in front of the Minara Agent runtime (`npm run serve`). Every route lives on `/v1/...` except the unauthenticated `/healthz` liveness probe. See https://docs.minara.ai/docs/reference/api/openapi for download instructions and tooling integration tips.",
    "license": {
      "name": "Apache-2.0"
    }
  },
  "servers": [
    {
      "url": "http://localhost:8080",
      "description": "Local dev (`npm run serve` default)"
    },
    {
      "url": "https://your-deployment.example.com",
      "description": "Replace with your deployment URL"
    }
  ],
  "components": {
    "securitySchemes": {
      "bearer": {
        "type": "http",
        "scheme": "bearer",
        "description": "Set `GATEWAY_AUTH_TOKEN` server-side; pass `Authorization: Bearer <token>` on every `/v1/...` route."
      }
    }
  },
  "security": [
    {
      "bearer": []
    }
  ],
  "tags": [
    {
      "name": "health"
    },
    {
      "name": "chat"
    },
    {
      "name": "state"
    },
    {
      "name": "auth"
    },
    {
      "name": "voice"
    },
    {
      "name": "files"
    },
    {
      "name": "research"
    },
    {
      "name": "config"
    },
    {
      "name": "workspace"
    },
    {
      "name": "stats"
    },
    {
      "name": "sessions"
    },
    {
      "name": "portfolio"
    },
    {
      "name": "perps"
    },
    {
      "name": "autopilot"
    },
    {
      "name": "personalization"
    },
    {
      "name": "learning"
    },
    {
      "name": "preferences"
    },
    {
      "name": "notifications"
    },
    {
      "name": "migrate"
    },
    {
      "name": "gateway"
    },
    {
      "name": "llm"
    },
    {
      "name": "market"
    },
    {
      "name": "skills"
    },
    {
      "name": "theme"
    },
    {
      "name": "admin"
    },
    {
      "name": "workflows"
    }
  ],
  "paths": {
    "/healthz": {
      "get": {
        "operationId": "healthz",
        "tags": [
          "health"
        ],
        "summary": "Health check",
        "description": "Liveness probe. Returns 200 with `{\"ok\": true}` if the gateway is up.",
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "ok": true
                }
              }
            }
          }
        },
        "security": []
      }
    },
    "/v1/chat": {
      "post": {
        "operationId": "chat",
        "tags": [
          "chat"
        ],
        "summary": "Synchronous chat",
        "description": "Single agent turn. The request body is queued, the agent runs to completion (or error), and the final assistant message is returned.\n\nLong-running. For interactive UIs, prefer `/v1/chat/stream` to see intermediate tool calls as they happen.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              },
              "example": {
                "message": "buy $50 of SOL"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "{ \"reply\": \"...\", \"toolCalls\": [ ... ] }",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/chat/stream": {
      "post": {
        "operationId": "chat_stream",
        "tags": [
          "chat"
        ],
        "summary": "Streaming chat (SSE)",
        "description": "Streaming agent turn over Server-Sent Events. Each SSE event is a JSON payload describing an intermediate step: LLM delta, tool call, tool result, or final assistant message.\n\nMid-turn steering events: a `user_interjection` event (`{ \"message\": \"...\", \"ts_ms\": 1747000000000 }`) is emitted when a message queued via `POST /v1/chat/interject` is injected into the running turn. The final `done` event data carries two optional fields: `interrupted: true` when the turn was stopped via `POST /v1/chat/interrupt`, and `pending_interjections: string[]` listing interjected messages that arrived too late to inject; clients re-send those as normal messages.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              },
              "example": {
                "message": "analyze BTC momentum"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "text/event-stream — see the SSE envelope defined in `apps/agent/src/gateway/api.ts`.",
            "content": {
              "text/event-stream": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/assist/system-prompt": {
      "post": {
        "operationId": "assist_system_prompt",
        "tags": [
          "chat"
        ],
        "summary": "Assist — generate Custom Agent system prompt (SSE)",
        "description": "Streams a Custom Agent system prompt for the Web UI wizard's Generate-with-AI panel. Body: `{ goal, name?, description? }`. Uses Anthropic Haiku with a 30s server-side timeout and 2000 max output tokens. Rate-limited to 10 requests per minute per token.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              },
              "example": {
                "goal": "monitor SOL volatility, alert if 1h move > 5%",
                "name": "SOL watcher",
                "description": "alerting"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "text/event-stream — `chunk` events carry `{ text }`; `done` and `error` mark stream end.",
            "content": {
              "text/event-stream": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/status": {
      "get": {
        "operationId": "status",
        "tags": [
          "state"
        ],
        "summary": "Agent status",
        "description": "Kill switch state, daily spend, tool count, review engine state. Equivalent to the `/status` REPL slash command.",
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/system-metrics": {
      "get": {
        "operationId": "system_metrics",
        "tags": [
          "state"
        ],
        "summary": "System resource metrics",
        "description": "Process + system snapshot for the Dashboard System status row: CPU percent, memory RSS, agent workspace size (recursive walk of `MINARA_DATA_DIR` — not host-filesystem usage) + sandbox folder size, gateway network throughput (bytes in/out per second), and recency-weighted LLM token throughput (averaged across the most recently active sessions, not lifetime). Each metric ships a 60-sample history for an inline sparkline. Returns `{ disabled: true }` when `MINARA_SYSTEM_METRICS_DISABLED=1`.",
        "responses": {
          "200": {
            "description": "{ \"ts\": 1747000000000, \"cpu\": { \"process_percent\": 18.4, \"system_load_1m\": 1.2, \"cores\": 8, \"history\": […] }, \"memory\": { \"process_rss_mb\": 642.1, \"system_used_mb\": 12480, \"system_total_mb\": 16384, \"h",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/dashboard/achievements": {
      "get": {
        "operationId": "dashboard_achievements",
        "tags": [
          "state"
        ],
        "summary": "Dashboard achievements",
        "description": "Marketing-style stats for the web-ui Dashboard banner: activation timestamp, distinct tickers researched, lifetime realized profit (positive deltas only — losses skipped), and estimated hours of research saved. Server caches the response for 60s; a successful sell / swap / close trade busts the cache.",
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/portfolio": {
      "get": {
        "operationId": "portfolio",
        "tags": [
          "state"
        ],
        "summary": "Current portfolio",
        "description": "Current portfolio snapshot from the Minara backend, formatted for agent consumption.",
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/methodologies": {
      "get": {
        "operationId": "methodologies",
        "tags": [
          "state"
        ],
        "summary": "Trading methodologies",
        "description": "List of registered methodologies with their pass/fail counts and recent outcomes.",
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/learning/audit/reports": {
      "get": {
        "operationId": "audit_reports",
        "tags": [
          "state"
        ],
        "summary": "Methodology audit reports",
        "description": "Trend + history of methodology audit health reports. Query `days` (default 30) bounds the look-back; returns lightweight summaries newest-first for the Learning Health trend chart.",
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/learning/audit/reports/latest": {
      "get": {
        "operationId": "audit_report_latest",
        "tags": [
          "state"
        ],
        "summary": "Latest methodology audit report",
        "description": "The most recent methodology audit report in full: composite health score, per-dimension scores, findings, and advisory actions. Returns `{ report, readiness }`. `report` is null until the first report exists; `readiness` carries the warm-up progress — the first report needs self-learning on plus a 7-day warm-up.",
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/learning/audit/reports/{passId}": {
      "get": {
        "operationId": "audit_report_detail",
        "tags": [
          "state"
        ],
        "summary": "Methodology audit report by id",
        "description": "One methodology audit report in full, looked up by its pass id.",
        "parameters": [
          {
            "in": "path",
            "name": "passId",
            "schema": {
              "type": "string"
            },
            "required": true
          }
        ],
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/learning/audit/run": {
      "post": {
        "operationId": "audit_run",
        "tags": [
          "state"
        ],
        "summary": "Run a methodology audit",
        "description": "Run one audit pass on demand and persist it. Optional body `{ windowDays }` (clamped 1-365) overrides the configured look-back. Returns `{ report, readiness }`; the first report is gated on the self-learning warm-up, so `report` is null (nothing persisted) until `readiness.eligible`. Returns 409 when an audit is already running.",
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/workflows": {
      "get": {
        "operationId": "workflows",
        "tags": [
          "state"
        ],
        "summary": "Workflows",
        "description": "List local workflow definitions and executions managed by the workflow engine.",
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "definitions_count": 1,
                  "executions_count": 2,
                  "definitions": [
                    {
                      "id": "wf_local_123",
                      "name": "daily-brief",
                      "backend": "local",
                      "publish_state": "published",
                      "publish_health": "up_to_date",
                      "active": true,
                      "has_unpublished_changes": false,
                      "last_execution_at": "2026-04-23T00:00:00.000Z",
                      "last_execution_status": "success",
                      "next_run_at": "2026-04-24T00:00:00.000Z"
                    }
                  ],
                  "executions": [
                    {
                      "id": "inst_123",
                      "definition_id": "wf_local_123",
                      "name": "daily-brief",
                      "execution_mode": "production",
                      "run_purpose": null,
                      "is_dry_run": false,
                      "status": "success",
                      "waiting_reason": null,
                      "cancel_reason": null,
                      "error_reason": null,
                      "iterations": 1,
                      "current_step": null,
                      "started_at": "2026-04-23T00:00:00.000Z",
                      "ended_at": "2026-04-23T00:00:03.000Z"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/kill": {
      "post": {
        "operationId": "kill",
        "tags": [
          "state"
        ],
        "summary": "Activate kill switch",
        "description": "Blocks all tier ≥ 2 tool calls until `/v1/unkill` is called. Intended for emergency halting.\n\nLogged to the audit trail.",
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/unkill": {
      "post": {
        "operationId": "unkill",
        "tags": [
          "state"
        ],
        "summary": "Deactivate kill switch",
        "description": "Clears the kill switch flag.",
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/auth/elevenlabs/api-key": {
      "post": {
        "operationId": "auth_elevenlabs_api_key",
        "tags": [
          "auth"
        ],
        "summary": "Save an ElevenLabs voice key",
        "description": "Save an ElevenLabs key for speech synthesis and transcription. Validated against the ElevenLabs /v1/user endpoint before persisting; pass `saveAnyway: true` to skip validation. Not an LLM provider — powers the voice endpoints only.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              },
              "example": {
                "apiKey": "...",
                "saveAnyway": false
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "ok": true,
                  "validated": true
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/voice/status": {
      "get": {
        "operationId": "voice_status",
        "tags": [
          "voice"
        ],
        "summary": "Voice service status",
        "description": "Whether speech services are configured: `stt_configured` (a transcription provider is available) and `tts_configured` (a synthesis provider is available). The web UI checks this before arming the microphone.",
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "ok": true,
                  "stt_configured": true,
                  "tts_configured": true
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/voice/voices": {
      "get": {
        "operationId": "voice_voices",
        "tags": [
          "voice"
        ],
        "summary": "List selectable voices",
        "description": "The voice-picker option list: official preset voices (ElevenLabs + OpenAI, always available) plus the user's own ElevenLabs voices fetched live when a key is configured. Each item carries `id`, `name`, `provider`, and `group` (`preset` or `custom`). The chosen id is saved separately to the `voice.ttsVoice` runtime preference.",
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "ok": true,
                  "voices": [
                    {
                      "id": "EpMOHcveL15wHOy6xt7T",
                      "name": "Minara",
                      "provider": "elevenlabs",
                      "group": "preset"
                    },
                    {
                      "id": "alloy",
                      "name": "Alloy",
                      "provider": "openai",
                      "group": "preset"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/voice/settings": {
      "get": {
        "operationId": "voice_settings_get",
        "tags": [
          "voice"
        ],
        "summary": "Get speech settings",
        "description": "Current user-tunable speech-synthesis settings (stability, similarity_boost, style, speaker boost, speed), merged onto the defaults. Drives the Settings -> Voice models sliders.",
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "ok": true,
                  "settings": {
                    "stability": 0.5,
                    "similarityBoost": 0.75,
                    "style": 0.3,
                    "speakerBoost": true,
                    "speed": 0.7
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      },
      "put": {
        "operationId": "voice_settings_put",
        "tags": [
          "voice"
        ],
        "summary": "Update speech settings",
        "description": "Persist user-tuned speech-synthesis settings. Accepts a partial object; every field is clamped to its valid range (0-1 for stability/similarity/style, 0.7-1.2 for speed). Returns the normalized result.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              },
              "example": {
                "stability": 0.5,
                "style": 0.3,
                "speed": 0.7
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "ok": true,
                  "settings": {
                    "stability": 0.5,
                    "similarityBoost": 0.75,
                    "style": 0.3,
                    "speakerBoost": true,
                    "speed": 0.7
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/voice/cleanup": {
      "post": {
        "operationId": "voice_cleanup",
        "tags": [
          "voice"
        ],
        "summary": "Clear old voice files",
        "description": "Delete persisted voice audio (spoken replies + mic recordings) on chat messages older than `older_than_days` (default 30, range 0-3650). The message rows stay; only the audio file + its metadata key are removed, so an old reply read aloud again re-synthesizes on demand. Returns counts + bytes freed.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              },
              "example": {
                "older_than_days": 30
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "ok": true,
                  "older_than_days": 30,
                  "deleted_files": 12,
                  "freed_bytes": 345678,
                  "messages_affected": 9
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/voice/transcribe": {
      "post": {
        "operationId": "voice_transcribe",
        "tags": [
          "voice"
        ],
        "summary": "Transcribe speech to text",
        "description": "Transcribe an uploaded audio recording via the configured voice provider (ElevenLabs Scribe preferred, OpenAI fallback). Optional `language` (ISO-639-1 hint) and `persist` (store the original recording, returning its `file_key`) form fields.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              },
              "example": "multipart/form-data"
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "ok": true,
                  "text": "...",
                  "file_key": "chat/files/..."
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/voice/tts": {
      "post": {
        "operationId": "voice_tts",
        "tags": [
          "voice"
        ],
        "summary": "Synthesize speech",
        "description": "Stream synthesized speech for up to 4096 chars of text. The provider body is piped through unbuffered, so first audio bytes arrive before synthesis completes. `format` is one of mp3 (default), opus, wav; opus routes to the OpenAI provider (Telegram-style voice notes). Sentence-by-sentence callers can pass `previous_text` / `next_text` (each truncated to 600 chars) so ElevenLabs keeps prosody continuous across requests, and `first_chunk: true` on a reply's first sentence to let the gateway swap in the fastest model when the faster-start setting is on.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              },
              "example": {
                "text": "...",
                "voice": "optional",
                "format": "mp3",
                "first_chunk": false,
                "previous_text": "optional",
                "next_text": "optional"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "audio bytes (audio/mpeg | audio/ogg | audio/wav)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/sessions/{id}/voice-audio": {
      "post": {
        "operationId": "session_voice_audio",
        "tags": [
          "voice"
        ],
        "summary": "Attach spoken-reply audio to a message",
        "description": "Attach a previously-uploaded audio file (POST /v1/files) to an assistant message's metadata so the session history can replay the exact spoken reply. `message_id` optional; defaults to the latest assistant message.",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "schema": {
              "type": "string"
            },
            "required": true
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              },
              "example": {
                "file_key": "chat/files/...",
                "message_id": 123
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "ok": true,
                  "message_id": 123
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/files": {
      "post": {
        "operationId": "files_upload",
        "tags": [
          "files"
        ],
        "summary": "Upload a file",
        "description": "Upload a file into the sandboxed workspace. Returns a key that can be referenced by tools via `read_file`.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              },
              "example": "multipart/form-data"
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "key": "...",
                  "size": 1234
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/files/{key}": {
      "get": {
        "operationId": "files_fetch",
        "tags": [
          "files"
        ],
        "summary": "Fetch a file",
        "description": "Stream a previously-uploaded sandboxed file back to the caller.",
        "parameters": [
          {
            "in": "path",
            "name": "key",
            "schema": {
              "type": "string"
            },
            "required": true
          }
        ],
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/artifacts/{id}": {
      "get": {
        "operationId": "artifacts_fetch",
        "tags": [
          "files"
        ],
        "summary": "Fetch an artifact",
        "description": "Fetch an agent-generated artifact (chart, spreadsheet, report payload) by its id. Artifacts are produced by document / visualization tools.",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "schema": {
              "type": "string"
            },
            "required": true
          }
        ],
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/artifacts": {
      "get": {
        "operationId": "artifacts_list",
        "tags": [
          "files"
        ],
        "summary": "List report artifacts",
        "description": "List completed report artifacts for the web UI's Files pages. Filter by `kind=chat|institution` to scope to one module's sessions, or pass `chat_id` for a single session. Each row includes the on-disk files plus pre-built `preview_url` (`/v1/sandbox/files/...?inline=1`) and `download_url` so the UI can join them with the gateway base.",
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/research": {
      "post": {
        "operationId": "research",
        "tags": [
          "research"
        ],
        "summary": "Deep research pipeline",
        "description": "Run the deep-research pipeline directly from the gateway without going through the chat loop. Equivalent to `minara research <topic>`.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              },
              "example": "{ \"topic\": \"...\", \"mode\": \"light\" | \"heavy\" }"
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/config/financial-profile": {
      "get": {
        "operationId": "config_financial_profile",
        "tags": [
          "config"
        ],
        "summary": "Get effective financial-profile config",
        "description": "Current resolved `FinancialProfileConfig` — useful when debugging why a rebuild gate fired (or didn't).",
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/auth/status": {
      "get": {
        "operationId": "auth_status",
        "tags": [
          "auth"
        ],
        "summary": "Auth status",
        "description": "List all configured auth profiles (OpenAI / Anthropic / OpenRouter / Minara).",
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/auth/oauth/openai/init": {
      "post": {
        "operationId": "auth_openai_init",
        "tags": [
          "auth"
        ],
        "summary": "OpenAI device-code init",
        "description": "Start an OpenAI Codex device-code login flow. Returns `{ flowId, userCode, verificationUrl, intervalSec }`. The web UI presents `userCode` + a button to open `verificationUrl`, then polls `/v1/auth/oauth/openai/poll?flowId=…` every `intervalSec`. Replaces the previous PKCE-loopback flow, which OpenAI's public Codex client_id rejects with `unknown_error` (only the device-code endpoints accept third-party integrations).",
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/auth/oauth/openai/poll": {
      "get": {
        "operationId": "auth_openai_poll",
        "tags": [
          "auth"
        ],
        "summary": "OpenAI device-code poll",
        "description": "Poll an in-flight OpenAI device-code flow by `flowId` (from `/v1/auth/oauth/openai/init`). Returns `{ status: \"pending\" }` until the user finishes sign-in, then `{ status: \"success\", accountId, expiresAt }` (tokens persisted) or `{ status: \"failed\", message }` on terminal upstream errors. Caches the success answer for the flow's 15-min TTL so a delayed re-poll after success still resolves cleanly.",
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/auth/oauth/openai/callback": {
      "get": {
        "operationId": "auth_openai_callback",
        "tags": [
          "auth"
        ],
        "summary": "OpenAI OAuth callback (retired)",
        "description": "Retired in 2026-05. The previous PKCE-loopback redirect target now returns 410 Gone with a hint pointing at the new device-code flow. Kept only so stale browser tabs opened before the cutover surface a clear message instead of \"Unknown or expired state\".",
        "responses": {
          "200": {
            "description": "Success"
          }
        },
        "security": []
      }
    },
    "/v1/auth/oauth/openrouter/init": {
      "post": {
        "operationId": "auth_openrouter_init",
        "tags": [
          "auth"
        ],
        "summary": "OpenRouter OAuth init",
        "description": "Start an OpenRouter OAuth PKCE flow.",
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/auth/oauth/openrouter/callback": {
      "get": {
        "operationId": "auth_openrouter_callback",
        "tags": [
          "auth"
        ],
        "summary": "OpenRouter OAuth callback",
        "description": "OAuth redirect target for OpenRouter.",
        "responses": {
          "200": {
            "description": "Success"
          }
        },
        "security": []
      }
    },
    "/v1/auth/openrouter/api-key": {
      "post": {
        "operationId": "auth_openrouter_api_key",
        "tags": [
          "auth"
        ],
        "summary": "OpenRouter direct API key",
        "description": "Skip the OAuth flow and register an OpenRouter API key directly.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              },
              "example": {
                "apiKey": "sk-or-..."
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/auth/oauth/anthropic/init": {
      "post": {
        "operationId": "auth_anthropic_init",
        "tags": [
          "auth"
        ],
        "summary": "Anthropic OAuth init",
        "description": "Start the Anthropic device-flow reuse path (reuses an existing Claude CLI login).",
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/auth/oauth/anthropic/exchange": {
      "post": {
        "operationId": "auth_anthropic_exchange",
        "tags": [
          "auth"
        ],
        "summary": "Anthropic OAuth exchange",
        "description": "Complete the Anthropic device flow by exchanging the verifier for credentials.",
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/auth/{provider}": {
      "delete": {
        "operationId": "auth_clear",
        "tags": [
          "auth"
        ],
        "summary": "Clear auth profile",
        "description": "Remove a stored auth profile. `:provider` is `openai` | `openai-api-key` | `anthropic` | `anthropic-api-key` | `openrouter` | `xai` | `ollama`.",
        "parameters": [
          {
            "in": "path",
            "name": "provider",
            "schema": {
              "type": "string"
            },
            "required": true
          }
        ],
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/auth/oauth/xai/init": {
      "post": {
        "operationId": "auth_xai_init",
        "tags": [
          "auth"
        ],
        "summary": "xAI OAuth init",
        "description": "Start a SuperGrok-style PKCE flow against auth.x.ai. The gateway binds the loopback callback port (56121) for the flow's lifetime via the in-process OAuth orchestrator; the web UI polls /v1/auth/status to detect completion.",
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/auth/oauth/flows/{flowId}": {
      "get": {
        "operationId": "auth_oauth_flow_status",
        "tags": [
          "auth"
        ],
        "summary": "Web OAuth flow status",
        "description": "Query the in-process orchestrator for the status of an in-flight web-OAuth flow. Returns `pending` / `success` / `failed` / `unknown`. Web UI uses this to surface precise failure reasons (token exchange errors, OAuth provider denials) before the 5-minute poll timeout.",
        "parameters": [
          {
            "in": "path",
            "name": "flowId",
            "schema": {
              "type": "string"
            },
            "required": true
          }
        ],
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      },
      "delete": {
        "operationId": "auth_oauth_flow_cancel",
        "tags": [
          "auth"
        ],
        "summary": "Cancel web OAuth flow",
        "description": "Cancel an in-flight web-OAuth flow — closes the bound loopback port immediately so the user (or a concurrent CLI login) can retry without waiting for the 5-minute timeout. Called by the web UI when the user closes the re-authorize modal.",
        "parameters": [
          {
            "in": "path",
            "name": "flowId",
            "schema": {
              "type": "string"
            },
            "required": true
          }
        ],
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/auth/openai/api-key": {
      "post": {
        "operationId": "auth_openai_api_key",
        "tags": [
          "auth"
        ],
        "summary": "OpenAI direct API key",
        "description": "Save an OpenAI platform API key (api.openai.com). Validates via GET /v1/models before persisting; pass `saveAnyway: true` to skip validation when offline. Requires gateway bearer auth (returns 503 when MINARA_HTTP_TOKEN is unset).",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              },
              "example": "{ \"apiKey\": \"sk-...\", \"saveAnyway\"?: boolean }"
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        },
        "security": [
          {
            "bearer": []
          }
        ]
      }
    },
    "/v1/auth/anthropic/api-key": {
      "post": {
        "operationId": "auth_anthropic_api_key",
        "tags": [
          "auth"
        ],
        "summary": "Anthropic direct API key",
        "description": "Save an Anthropic platform API key. Validates via GET /v1/models before persisting. Lives in a dedicated `anthropicApiKey` profile slot — survives gateway restart without depending on $dataDir/env being sourced.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              },
              "example": "{ \"apiKey\": \"sk-ant-...\", \"saveAnyway\"?: boolean }"
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        },
        "security": [
          {
            "bearer": []
          }
        ]
      }
    },
    "/v1/auth/xai/api-key": {
      "post": {
        "operationId": "auth_xai_api_key",
        "tags": [
          "auth"
        ],
        "summary": "xAI direct API key",
        "description": "Save an xAI (Grok) API key. Validates via GET /v1/models before persisting.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              },
              "example": "{ \"apiKey\": \"xai-...\", \"saveAnyway\"?: boolean }"
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        },
        "security": [
          {
            "bearer": []
          }
        ]
      }
    },
    "/v1/auth/ollama/api-key": {
      "post": {
        "operationId": "auth_ollama_api_key",
        "tags": [
          "auth"
        ],
        "summary": "Ollama Cloud API key",
        "description": "Save an Ollama Cloud API key. Validates the key against the configured Ollama host's `/api/tags` before persisting; pass `saveAnyway: true` to store an unverified key.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              },
              "example": "{ \"apiKey\": \"...\", \"saveAnyway\"?: boolean }"
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        },
        "security": [
          {
            "bearer": []
          }
        ]
      }
    },
    "/v1/auth/preferred-provider": {
      "get": {
        "operationId": "auth_preferred_provider_get",
        "tags": [
          "auth"
        ],
        "summary": "Get preferred provider",
        "description": "Read the user's explicit \"Use this provider\" override from auth-profiles.json. select-provider.ts honors this BEFORE its normal precedence chain.",
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      },
      "put": {
        "operationId": "auth_preferred_provider_put",
        "tags": [
          "auth"
        ],
        "summary": "Set preferred provider (hot-swap)",
        "description": "Set or clear the preferred provider override. Live-probes the selected credential before persisting; returns 400 `reauth_required` when the profile is configured but the credential is stale.\n\nOn success, ALSO performs an in-process hot-swap: rebuilds the LLM client for the new provider via `selectProvider` + `createLLMClient` and atomically updates `LLMRouter` (the single source of truth). Long-lived consumers (AgentLoop, ScenarioClassifier, ChartBuilder, …) receive a live Proxy and pick up the new client on next createMessage. AgentLoop snapshots the client at turn entry, so a turn in flight when the swap lands finishes on the OLD provider (no split conversation); the NEXT turn uses the new provider.\n\nModel: auto-picks `defaultModelForProvider(newKind)` since model ids are provider-scoped (`grok-4` only works on xAI, `gpt-5` only on OpenAI). The new model is persisted under the `defaultModel` slot keyed by the new provider, so the next boot starts on the same pair.\n\nResponse: `{ ok, preferredProvider, activeModel, swapped, inflightOnOldProvider }`. `swapped: false` indicates the preference was persisted but the in-process rebuild failed (e.g. credentials disappeared between probe and rebuild); the next gateway boot still picks up the persisted preference.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              },
              "example": "{ \"provider\": ProviderKind | null }"
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        },
        "security": [
          {
            "bearer": []
          }
        ]
      }
    },
    "/v1/workspace/files": {
      "get": {
        "operationId": "workspace_files_list",
        "tags": [
          "workspace"
        ],
        "summary": "List workspace files",
        "description": "Enumerate every editable workspace md file with size, mtime, and sha256. Files that don't exist on disk show `exists: false`. The name set is whitelisted in `apps/agent/src/workspace/seed.ts` (`EDITABLE_WORKSPACE_FILES`).",
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "ok": true,
                  "data": {
                    "workspace_dir": "/home/user/.minara/workspace",
                    "files": [
                      {
                        "name": "SOUL.md",
                        "exists": true,
                        "size": 1982,
                        "mtime": 1714492800000,
                        "sha256": "deadbeef..."
                      },
                      {
                        "name": "HEARTBEAT.md",
                        "exists": false,
                        "size": 0,
                        "mtime": null,
                        "sha256": null
                      }
                    ]
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/workspace/files/{name}": {
      "get": {
        "operationId": "workspace_file_get",
        "tags": [
          "workspace"
        ],
        "summary": "Read a workspace file",
        "description": "Return the contents of a single workspace md file plus its sha256. `:name` must be a whitelisted filename (SOUL.md / AGENTS.md / IDENTITY.md / USER.md / MEMORY.md / BOOTSTRAP.md / TOOLS.md / HEARTBEAT.md). Path traversal attempts (`..`, absolute paths, non-whitelisted names) return 404.",
        "parameters": [
          {
            "in": "path",
            "name": "name",
            "schema": {
              "type": "string"
            },
            "required": true
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "ok": true,
                  "data": {
                    "name": "SOUL.md",
                    "exists": true,
                    "content": "# SOUL.md - Minara\n...",
                    "sha256": "deadbeef...",
                    "mtime": 1714492800000
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      },
      "put": {
        "operationId": "workspace_file_put",
        "tags": [
          "workspace"
        ],
        "summary": "Write a workspace file (sha256 OCC)",
        "description": "Atomically replace a workspace md file. `expected_sha256` is REQUIRED — pass the value returned by the previous GET (string), or `null` to create a brand-new file. Mismatch returns 409 with the live sha so the UI can offer overwrite / reload. Body cap is 256 KB; oversize requests return 413.\n\nOptimistic concurrency: the gateway compares `expected_sha256` to the on-disk sha BEFORE writing. A miss yields 409. The atomic write goes through `apps/agent/src/workspace/seed.ts::atomicWriteFile`, which writes to a tempfile and renames into place, with a single-slot `.bak` backup written next to the target.",
        "parameters": [
          {
            "in": "path",
            "name": "name",
            "schema": {
              "type": "string"
            },
            "required": true
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              },
              "example": {
                "content": "# SOUL.md - Minara\n...",
                "expected_sha256": "deadbeef..."
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "ok": true,
                  "data": {
                    "name": "SOUL.md",
                    "sha256": "cafef00d...",
                    "size": 2048,
                    "mtime": 1714493000000
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/workspace/files/{name}/restore-template": {
      "post": {
        "operationId": "workspace_file_restore_template",
        "tags": [
          "workspace"
        ],
        "summary": "Reset a workspace file to its shipped template",
        "description": "Overwrite the workspace file with the shipped template from `src/workspace/templates/`. Useful when an operator wants to discard local edits and restart from the default. Files without a shipped template (HEARTBEAT.md is runtime-only) return 422.",
        "parameters": [
          {
            "in": "path",
            "name": "name",
            "schema": {
              "type": "string"
            },
            "required": true
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "ok": true,
                  "data": {
                    "name": "SOUL.md",
                    "restored": true
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/healthz": {
      "get": {
        "operationId": "v1_healthz",
        "tags": [
          "health"
        ],
        "summary": "Versioned health check",
        "description": "Versioned mirror of `/healthz`. Returns `{\"ok\": true, \"ts\": ...}` without auth so external monitors can probe behind a `GATEWAY_AUTH_TOKEN`.",
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "ok": true,
                  "ts": 1747333200000
                }
              }
            }
          }
        },
        "security": []
      }
    },
    "/v1/chat/stream/resume": {
      "get": {
        "operationId": "chat_stream_resume",
        "tags": [
          "chat"
        ],
        "summary": "Resume a streaming chat",
        "description": "Reattach to an in-flight `/v1/chat/stream` SSE buffer that was interrupted by a network blip. Pass the original `correlation_id` as a query parameter; the gateway replays the buffered events from the last delivered cursor.",
        "parameters": [
          {
            "in": "query",
            "name": "correlation_id",
            "schema": {
              "type": "string"
            },
            "required": true
          },
          {
            "in": "query",
            "name": "cursor",
            "schema": {
              "type": "string"
            },
            "required": false
          }
        ],
        "responses": {
          "200": {
            "description": "text/event-stream — same envelope as `/v1/chat/stream`.",
            "content": {
              "text/event-stream": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/chat/interject": {
      "post": {
        "operationId": "chat_interject",
        "tags": [
          "chat"
        ],
        "summary": "Interject into a running turn",
        "description": "Queue a steering message for the session's in-flight turn. The gateway injects it into the running turn at the next step boundary, and the model folds it into its current work instead of starting a fresh turn. The live `/v1/chat/stream` connection echoes the injection as a `user_interjection` event (`{ message, ts_ms }`).\n\nUse this when the user types a follow-up while a long turn is still streaming and the message should steer the work in progress (narrow the scope, add a constraint, correct a wrong assumption) rather than abort it.\n\nResponses: `200` with `{ \"status\": \"injected\" }` when the message was queued for the in-flight turn; `409` with `{ \"status\": \"no_active_turn\" }` when no turn is running for the session, in which case the client sends the message as a normal `POST /v1/chat/stream` request instead; `400` when `session_id` or `message` is missing or empty.\n\nA message that is queued but arrives too late to inject comes back on the stream's final `done` event under `pending_interjections`; clients re-send those as normal messages.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              },
              "example": {
                "session_id": "sess_abc",
                "message": "skip the 1h chart, focus on funding rates"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "injected"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/chat/interrupt": {
      "post": {
        "operationId": "chat_interrupt",
        "tags": [
          "chat"
        ],
        "summary": "Interrupt a running turn",
        "description": "Stop the session's in-flight turn. The turn ends gracefully: partial work is kept, and the SSE stream finishes with a `done` event carrying `interrupted: true`.\n\nBacks a user-facing Stop button. The turn does not error out: text and tool results produced before the interrupt persist to the session transcript, and the stream still closes with a normal `done` event (flagged `interrupted: true`).\n\nIdempotent: returns `200` with `{ \"status\": \"interrupted\" }` when a turn was stopped, and `200` with `{ \"status\": \"no_active_turn\" }` when nothing was running. `400` when `session_id` is missing.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              },
              "example": {
                "session_id": "sess_abc"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "interrupted"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/stats": {
      "get": {
        "operationId": "stats",
        "tags": [
          "stats"
        ],
        "summary": "Per-session usage stats",
        "description": "Token + cost + tool-call counters bucketed by chat session.",
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/telemetry/block-action": {
      "post": {
        "operationId": "telemetry_block_action",
        "tags": [
          "stats"
        ],
        "summary": "UI Block click telemetry",
        "description": "Fire-and-forget click record for a `ui_block` event (UBP v1). Posted by the web-ui when a user clicks a CTA inside a rendered block such as `minara.feature_recommendation@1`. The gateway logs the structured payload and returns `204 No Content`; analytics pipelines attach to the log line. Failure is silent client-side — the user-visible action runs regardless.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              },
              "example": {
                "blockId": "b_abc123",
                "itemId": "open_deposit_modal",
                "action": {
                  "kind": "modal",
                  "name": "deposit"
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "(empty — 204 No Content)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/sessions": {
      "get": {
        "operationId": "sessions_list",
        "tags": [
          "sessions"
        ],
        "summary": "List chat sessions",
        "description": "List the user's chat sessions with last-activity timestamp and turn count. Used by the web-UI session sidebar.",
        "parameters": [
          {
            "in": "query",
            "name": "limit",
            "schema": {
              "type": "integer",
              "example": 50
            },
            "required": false
          }
        ],
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      },
      "post": {
        "operationId": "sessions_create",
        "tags": [
          "sessions"
        ],
        "summary": "Create a chat session",
        "description": "Provision a new session id; the agent loop scopes per-session state (frozen snapshot, scenario cache, shadow recorder) to this id.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              },
              "example": {
                "title": "BTC analysis"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "ok": true,
                  "data": {
                    "id": "sess_abc",
                    "created_at": "..."
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/portfolio/status": {
      "get": {
        "operationId": "portfolio_status",
        "tags": [
          "portfolio"
        ],
        "summary": "Portfolio connectivity status",
        "description": "Quick readiness probe: whether Minara is authenticated and which venues are reachable. Cheap; safe to poll.",
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/portfolio/spot": {
      "get": {
        "operationId": "portfolio_spot",
        "tags": [
          "portfolio"
        ],
        "summary": "Spot balances",
        "description": "Aggregate spot balances across every connected wallet, normalized to the gateway's `PortfolioAsset` shape.",
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/portfolio/spot/positions": {
      "get": {
        "operationId": "portfolio_spot_positions",
        "tags": [
          "portfolio"
        ],
        "summary": "Spot positions",
        "description": "Per-asset spot positions including avg cost basis when known.",
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/portfolio/perps": {
      "get": {
        "operationId": "portfolio_perps",
        "tags": [
          "portfolio"
        ],
        "summary": "Perps positions",
        "description": "Open perp positions across every Minara perp sub-wallet. Use `?subAccountId=` to scope to one sub.",
        "parameters": [
          {
            "in": "query",
            "name": "subAccountId",
            "schema": {
              "type": "string"
            },
            "required": false
          }
        ],
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/portfolio/perps/summary": {
      "get": {
        "operationId": "portfolio_perps_summary",
        "tags": [
          "portfolio"
        ],
        "summary": "Perps account summary",
        "description": "Aggregated equity / margin / unrealized PnL across perp subs.",
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/portfolio/history": {
      "get": {
        "operationId": "portfolio_history",
        "tags": [
          "portfolio"
        ],
        "summary": "Portfolio P&L history",
        "description": "Timeseries of total portfolio value bucketed for chart display.",
        "parameters": [
          {
            "in": "query",
            "name": "type",
            "schema": {
              "type": "string",
              "example": "1d"
            },
            "required": false
          }
        ],
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/portfolio/spot/activity": {
      "get": {
        "operationId": "portfolio_spot_activity",
        "tags": [
          "portfolio"
        ],
        "summary": "Recent spot activity",
        "description": "Cross-chain spot activity feed (swaps + transfers) for the user's primary wallet, sourced from Minara `/v1/tx/cross-chain/activities`.",
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/perps/account-state": {
      "get": {
        "operationId": "perps_account_state",
        "tags": [
          "perps"
        ],
        "summary": "Hyperliquid account state",
        "description": "Live state for one Hyperliquid sub: equity, margin usage, withdrawable, open orders count.",
        "parameters": [
          {
            "in": "query",
            "name": "subAccountId",
            "schema": {
              "type": "string"
            },
            "required": true
          }
        ],
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/perps/sub-accounts": {
      "get": {
        "operationId": "perps_sub_accounts",
        "tags": [
          "perps"
        ],
        "summary": "List perp sub-accounts",
        "description": "All perp sub-wallets under the user's Minara perp wallet, with the multi-exchange binding (`exchange`: hyperliquid | lighter), lifecycle `status` (active | creating | error), default flag, and a `schema_source` marker that callers MUST consult before making safety decisions on the bound exchange.",
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      },
      "post": {
        "operationId": "perp_wallets_create_alias",
        "tags": [
          "perps"
        ],
        "summary": "Create a perp sub-account (alias)",
        "description": "Agent-native alias of `POST /v1/perp-wallets`. Same body, same response shape.",
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/perp-wallets": {
      "get": {
        "operationId": "perp_wallets_list_alias",
        "tags": [
          "perps"
        ],
        "summary": "List perp sub-accounts (alias)",
        "description": "Same handler as `/v1/perps/sub-accounts`; aligned with the upstream OpenAPI path so future SDKs / direct API consumers can use the canonical name. Web-UI continues to call the agent-native path; new clients should target this one.",
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      },
      "post": {
        "operationId": "perp_wallets_create",
        "tags": [
          "perps"
        ],
        "summary": "Create a perp sub-account",
        "description": "Create a new perp sub-account bound to one exchange (`hyperliquid` | `lighter`). Body: `{ name: string (max 20), exchange?: 'hyperliquid' | 'lighter' }`. Returns the new sub-account in `creating` status — callers MUST poll `GET /v1/perp-wallets` until it reaches `active` before issuing trading-gateway calls against it. Also accepts the `/v1/perps/sub-accounts` alias.",
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/perp-wallets/rename": {
      "post": {
        "operationId": "perp_wallets_rename",
        "tags": [
          "perps"
        ],
        "summary": "Rename a perp sub-account",
        "description": "Rename an existing perp sub-account. Body: `{ subAccountId: string, name: string (max 20) }`. Read-name-only change; does not affect the on-chain address or the bound exchange.",
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/perps/sub-accounts/rename": {
      "post": {
        "operationId": "perp_wallets_rename_alias",
        "tags": [
          "perps"
        ],
        "summary": "Rename a perp sub-account (alias)",
        "description": "Agent-native alias of `POST /v1/perp-wallets/rename`.",
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/trading-gateway/place-orders": {
      "post": {
        "operationId": "trading_gateway_place_orders",
        "tags": [
          "perps"
        ],
        "summary": "Place perp orders",
        "description": "Batch place perp orders against the sub-account's bound exchange. Each order uses an action-based DTO (`open_long` | `open_short` | `close_long` | `close_short` | `set_stop_loss` | `set_take_profit`) with optional nested `stop_loss_orders` / `take_profit_orders` arrays for first-class TpSL. Body: `{ orders: BatchOrderItem[], subAccountId?: string }`. Returns `BatchPlaceOrderResult[]` — one per input order with per-order success / failure so a partially-rejected batch doesn't fail the whole call.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              },
              "example": {
                "orders": [
                  {
                    "action": "open_long",
                    "symbol": "BTC",
                    "quantity": "0.01",
                    "leverage": 5,
                    "stop_loss_orders": [
                      {
                        "price": "55000"
                      }
                    ],
                    "take_profit_orders": [
                      {
                        "price": "75000"
                      }
                    ]
                  }
                ],
                "subAccountId": "sub_hl_default"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": [
                  {
                    "success": true,
                    "order": {
                      "order_id": "ord_abc",
                      "symbol": "BTC",
                      "side": "buy",
                      "price": "62000",
                      "quantity": "0.01",
                      "status": "open",
                      "created_at": 1716796800000,
                      "raw_data": {}
                    }
                  }
                ]
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/trading-gateway/cancel-orders": {
      "post": {
        "operationId": "trading_gateway_cancel_orders",
        "tags": [
          "perps"
        ],
        "summary": "Cancel perp orders",
        "description": "Batch cancel by `{ symbol, orderId }`. Body: `{ cancels: { symbol: string, orderId: string }[], subAccountId?: string }`. Returns the list of cancelled order ids plus the upstream `raw_data` for audit.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              },
              "example": {
                "cancels": [
                  {
                    "symbol": "BTC",
                    "orderId": "ord_abc"
                  }
                ],
                "subAccountId": "sub_hl_default"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "cancelled_order_ids": [
                    "ord_abc"
                  ],
                  "raw_data": {}
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/trading-gateway/modify-order": {
      "post": {
        "operationId": "trading_gateway_modify_order",
        "tags": [
          "perps"
        ],
        "summary": "Modify an active order",
        "description": "Modify an active order's price, quantity, or trigger price. Body: `{ symbol: string, orderId: string, price?: string, quantity?: string, triggerPrice?: string, subAccountId?: string }` — at least one of `price` / `quantity` / `triggerPrice` must be supplied or the call is a no-op. Returns the updated `PlaceOrderResult`.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              },
              "example": {
                "symbol": "BTC",
                "orderId": "ord_abc",
                "price": "63500",
                "subAccountId": "sub_hl_default"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/trading-gateway/update-leverage": {
      "post": {
        "operationId": "trading_gateway_update_leverage",
        "tags": [
          "perps"
        ],
        "summary": "Update leverage + margin mode",
        "description": "Change leverage and margin mode (cross vs isolated) on a symbol in one round-trip. Body: `{ symbol: string, leverage: number, isCross: boolean, subAccountId?: string }`. Returns `{ leverage, is_cross, raw_data }`.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              },
              "example": {
                "symbol": "BTC",
                "leverage": 5,
                "isCross": true,
                "subAccountId": "sub_hl_default"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "leverage": 5,
                  "is_cross": true,
                  "raw_data": {}
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/trading-gateway/update-isolated-margin": {
      "post": {
        "operationId": "trading_gateway_update_isolated_margin",
        "tags": [
          "perps"
        ],
        "summary": "Add or remove isolated margin",
        "description": "Adjust the isolated-margin amount on an existing position. Body: `{ symbol: string, isAdd: boolean, amount: string, subAccountId?: string }`. Set `isAdd: true` to top up, `false` to claw back. Returns `{ amount, is_add, raw_data }`.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              },
              "example": {
                "symbol": "BTC",
                "isAdd": true,
                "amount": "100",
                "subAccountId": "sub_hl_default"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "amount": "100",
                  "is_add": true,
                  "raw_data": {}
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/trading-gateway/withdraw": {
      "post": {
        "operationId": "trading_gateway_withdraw",
        "tags": [
          "perps"
        ],
        "summary": "Withdraw USDC from a perp sub-account",
        "description": "Withdraw USDC to an external address. Body: `{ amount: string, toAddress: string, subAccountId?: string, totpCode: string }`. `totpCode` is required by the upstream `UserTOTPGuard`; users with TOTP disabled may pass an empty string. Returns `{ amount, to, raw_data }`. Tx hash is extracted from `raw_data.tx_hash` (or nested under `raw_data.receipt.txHash`) by the agent's withdraw watcher.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              },
              "example": {
                "amount": "50",
                "toAddress": "0xabcd...",
                "subAccountId": "sub_hl_default",
                "totpCode": "123456"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "amount": "50",
                  "to": "0xabcd...",
                  "raw_data": {}
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/trading-gateway/summary": {
      "get": {
        "operationId": "trading_gateway_summary",
        "tags": [
          "perps"
        ],
        "summary": "Per-sub equity + positions + open orders",
        "description": "One-shot read for a single sub-wallet's equity, open positions, and open orders. `subAccountId` is optional — when omitted the user's default sub is used. Routes per the sub's bound exchange. Prefer this over the legacy `/v1/perp-wallets/summary` (kept for backwards compat); the legacy path requires `subAccountId` and historically only spoke to Hyperliquid's default dex.",
        "parameters": [
          {
            "in": "query",
            "name": "subAccountId",
            "schema": {
              "type": "string"
            },
            "required": false
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "total_equity": "175.52",
                  "available_balance": "90.18",
                  "margin_used": "85.34",
                  "positions": [],
                  "open_orders": []
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/security/totp/status": {
      "get": {
        "operationId": "security_totp_status",
        "tags": [
          "auth"
        ],
        "summary": "TOTP status",
        "description": "Projection of `/auth/me` through the agent's `normalizeTotpStatus` adapter. Returns the three boolean TOTP toggles (master enabled, withdraw-required, new-device-required) plus a `schema_source` provenance marker. Safety-critical callers (withdraw preview, login flow) MUST fail-closed when `schema_source === \"indeterminate\"`.",
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/user/wallet-addresses": {
      "get": {
        "operationId": "user_wallet_addresses",
        "tags": [
          "auth"
        ],
        "summary": "User wallet addresses",
        "description": "Verbatim pass-through of `/auth/me`'s `wallets` object, filtered to string-valued entries. Returns a flat record keyed by `<purpose>-<chain>` (e.g. `spot-evm`, `spot-solana`, `abstraction-evm`, `earn-evm`, `perpetual-evm`). The Portfolio Spot Value card surfaces `spot-evm` + `spot-solana` as click-to-copy chips; other keys are forwarded for future surfaces. The nested `whitelist` object on the upstream payload is dropped (only addresses come through). New purposes / chains may extend the key set; clients must tolerate unknown keys. **Always returns HTTP 200**, even on upstream failure (the UI polls this every 5 minutes and `usePolling` would otherwise preserve the last successful payload across an error, leaking a logged-out user's previous-session addresses).",
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/user/profile": {
      "get": {
        "operationId": "user_profile",
        "tags": [
          "auth"
        ],
        "summary": "User profile",
        "description": "Display identity for the account menu. Projects the signed-in Minara account's `/auth/me` into `{ avatar, name, subscription }` — the avatar URL, display name, and subscription standing (plan, a paid flag, remaining/total credits, and expiry). Each field is omitted when the upstream payload lacks it. **Always returns HTTP 200**, even on upstream failure, so the UI falls back to an initials avatar instead of surfacing an error.",
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/security/totp/generate": {
      "post": {
        "operationId": "security_totp_generate",
        "tags": [
          "auth"
        ],
        "summary": "Generate TOTP secret",
        "description": "Mints a fresh TOTP secret + QR code URL + backup codes for the user to add to an authenticator app. Step 2 of the Settings → Security enable wizard.",
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/security/totp/enable": {
      "post": {
        "operationId": "security_totp_enable",
        "tags": [
          "auth"
        ],
        "summary": "Enable TOTP",
        "description": "Activates TOTP after the user proves they configured the authenticator by submitting a fresh 6-digit code. Step 3 of the enable wizard.",
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/security/totp/disable": {
      "post": {
        "operationId": "security_totp_disable",
        "tags": [
          "auth"
        ],
        "summary": "Disable TOTP",
        "description": "Turns TOTP off without removing the device binding. Requires both a fresh TOTP code AND a fresh email verification code so a stolen authenticator alone cannot disable 2FA.",
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/security/totp/verify": {
      "post": {
        "operationId": "security_totp_verify",
        "tags": [
          "auth"
        ],
        "summary": "Verify TOTP code",
        "description": "Agent-internal verifier used by the Settings page to confirm the user can read a current TOTP code (e.g. before unbinding).",
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/security/totp/unbind": {
      "post": {
        "operationId": "security_totp_unbind",
        "tags": [
          "auth"
        ],
        "summary": "Unbind TOTP device",
        "description": "Irrevocably removes the authenticator binding — re-enabling requires a fresh scan + verify cycle. Same dual-factor (TOTP + email) requirement as disable.",
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/security/email-code": {
      "post": {
        "operationId": "security_email_code",
        "tags": [
          "auth"
        ],
        "summary": "Send email verification code",
        "description": "Triggers `POST /auth/email/code` upstream so the user can receive a fresh verification code. Disable/unbind TOTP flows pair the authenticator code with this email code; other account changes that need a fresh email code can also call this endpoint. Body accepts an optional `emailType` string to scope the template; omit for the generic verification.",
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/security/totp/settings": {
      "put": {
        "operationId": "security_totp_settings",
        "tags": [
          "auth"
        ],
        "summary": "Update TOTP settings",
        "description": "Independently toggle new-device-login and withdraw TOTP requirements. The withdraw toggle gates the perps withdraw TOTP prompt; the new-device toggle gates first-login TOTP on a fresh browser / CLI. Requires a fresh TOTP code to authenticate the settings change itself.",
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/wallet/deposits": {
      "get": {
        "operationId": "wallet_deposits",
        "tags": [
          "perps"
        ],
        "summary": "Deposit options (spot + perps)",
        "description": "One envelope with spot + perps deposit addresses. Spot maps from the user's cross-chain account (one address, many chains); each perps target is a Hyperliquid sub-account with the USDC-on-Arbitrum constraint hard-coded. Read-only — no fund-moving gate. Used by the Portfolio DepositModal.",
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/wallet/supported-chains": {
      "get": {
        "operationId": "wallet_supported_chains",
        "tags": [
          "perps"
        ],
        "summary": "Wallet-supported chains",
        "description": "Proxy of the upstream Minara `/tokens/supported-chains` endpoint. Returns the list of chains the wallet backend can credit deposits on (EVM family + Solana). Used by the Portfolio Deposit modal to filter the chain picker so users can't pick a chain that would result in lost funds. Response is stable across releases; the gateway caches in-process for 24h and the web UI loads once at app startup. On upstream failure the endpoint returns an empty list rather than stale data — the wallet picker prefers to show nothing over showing the wrong thing.",
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/wallet/deposits/watch": {
      "post": {
        "operationId": "wallet_deposits_watch",
        "tags": [
          "perps"
        ],
        "summary": "Watch a perps deposit address (SSE)",
        "description": "Server-sent-events stream tracking real-time deposit detection for one perps USDC/Arbitrum address. Emits `watching → detected_on_chain → confirming → credited / failed / timeout`. Behind the `WALLET_DEPOSIT_WATCH_ENABLED` env flag. Transport auto-selects Alchemy SDK when `ALCHEMY_API_KEY` is set, else public Arbitrum RPC.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              },
              "example": {
                "address": "0xabc...",
                "asset": "USDC",
                "chain": "arbitrum"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/wallet/withdraw/spot": {
      "post": {
        "operationId": "wallet_withdraw_spot",
        "tags": [
          "perps"
        ],
        "summary": "Spot withdraw — preview / execute",
        "description": "Preview a cross-chain spot withdraw (`confirm: false`) or execute it (`confirm: true`, with `quoteHash` + `idempotencyKey` echoed from the preview). Execute currently returns 501 `spot_execute_not_yet_supported` — a per-(symbol, chain) token contract registry is still pending. Preview works fully.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              },
              "example": {
                "confirm": false,
                "token": "USDC",
                "chain": "arbitrum",
                "address": "0x...",
                "amount": "10.5"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/wallet/withdraw/perps": {
      "post": {
        "operationId": "wallet_withdraw_perps",
        "tags": [
          "perps"
        ],
        "summary": "Perps withdraw — preview / execute",
        "description": "Preview a perps USDC/Arbitrum withdraw (`confirm: false`) or execute it (`confirm: true`, with `quoteHash` + `idempotencyKey`). Execute currently returns 501 `perps_execute_not_yet_supported` because upstream `/v1/tx/perps/withdraw` has no sub-account or destination field. Preview works fully and binds the source sub-account into the quote hash.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              },
              "example": {
                "confirm": false,
                "walletId": "0x...",
                "address": "0x...",
                "amount": "10.5",
                "asset": "USDC"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/wallet/withdrawals/{operationId}": {
      "get": {
        "operationId": "wallet_withdrawals_status",
        "tags": [
          "perps"
        ],
        "summary": "Get withdraw operation status",
        "description": "Durable status lookup for a withdraw operation. Returns the latest known `{operationId, txHash?, status, broadcastAt, confirmedAt?, failedReason?}` from the local `withdraw_operations` table. Lets the UI reconnect to an in-flight withdraw after the modal closes.",
        "parameters": [
          {
            "in": "path",
            "name": "operationId",
            "schema": {
              "type": "string"
            },
            "required": true
          }
        ],
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/wallet/withdrawals/{operationId}/watch": {
      "post": {
        "operationId": "wallet_withdrawals_watch",
        "tags": [
          "perps"
        ],
        "summary": "Watch a withdraw operation (SSE)",
        "description": "Server-sent-events stream tracking outbound tx finality for one withdraw operationId. Emits `broadcast → confirming → credited / failed / timeout`. Terminal events persist back to the durable status table so the status endpoint reflects the real outcome.",
        "parameters": [
          {
            "in": "path",
            "name": "operationId",
            "schema": {
              "type": "string"
            },
            "required": true
          }
        ],
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/perps/trades": {
      "get": {
        "operationId": "perps_trades",
        "tags": [
          "perps"
        ],
        "summary": "Recent perps fills",
        "description": "Hyperliquid fill history for one sub-wallet (live, not the local mirror). Mirrors the upstream `userFills` payload through the gateway's `normalizeFill`.",
        "parameters": [
          {
            "in": "query",
            "name": "subAccountId",
            "schema": {
              "type": "string"
            },
            "required": true
          },
          {
            "in": "query",
            "name": "limit",
            "schema": {
              "type": "integer",
              "example": 100
            },
            "required": false
          }
        ],
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/perps/funding": {
      "get": {
        "operationId": "perps_funding",
        "tags": [
          "perps"
        ],
        "summary": "Funding history",
        "description": "Funding payments received / paid per perp position.",
        "parameters": [
          {
            "in": "query",
            "name": "subAccountId",
            "schema": {
              "type": "string"
            },
            "required": true
          }
        ],
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/perps/order-history": {
      "get": {
        "operationId": "perps_order_history",
        "tags": [
          "perps"
        ],
        "summary": "Order history",
        "description": "Recently filled / cancelled orders.",
        "parameters": [
          {
            "in": "query",
            "name": "subAccountId",
            "schema": {
              "type": "string"
            },
            "required": true
          }
        ],
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/perps/lighter-pnl": {
      "get": {
        "operationId": "perps_lighter_pnl",
        "tags": [
          "perps"
        ],
        "summary": "Lighter sub-wallet realized PnL (24h / 7d / 30d)",
        "description": "Rolling realized PnL for a Lighter sub-wallet, fetched from Lighter's first-class `/api/v1/pnl` endpoint (1h + 1d resolutions). The handler owns the encrypted-bearer handshake against Minara's `/v1/tx/perps/lighter-authorization` (RSA-OAEP-256 + AES-256-GCM) so the web UI calls one clean endpoint and gets back `{ d1, d7, d30, connected }`. Each window value is `null` when Lighter has no data inside it — the Portfolio Perps Value card renders `—` for nulls. Fail-open: any step (sub-account lookup, authorization fetch, decrypt, Lighter HTTP call) degrades to per-window `null` with a warn log. HL-bound sub-wallets aren't this endpoint's audience — they use the trade + funding feeds client-side; calling this with an HL `?wallet=` returns the empty envelope.",
        "parameters": [
          {
            "in": "query",
            "name": "wallet",
            "schema": {
              "type": "string"
            },
            "required": false
          }
        ],
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/orders": {
      "get": {
        "operationId": "orders_open",
        "tags": [
          "perps"
        ],
        "summary": "Open orders",
        "description": "Currently resting orders across every perp sub-wallet.",
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/autopilot/strategies": {
      "get": {
        "operationId": "autopilot_strategies",
        "tags": [
          "autopilot"
        ],
        "summary": "Autopilot strategies",
        "description": "List user-deployed autopilot strategies + their on/off state.",
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/autopilot/records": {
      "get": {
        "operationId": "autopilot_records",
        "tags": [
          "autopilot"
        ],
        "summary": "Autopilot execution records",
        "description": "Recent autopilot decision + execution log. Useful for the Autopilot tab in the web UI.",
        "parameters": [
          {
            "in": "query",
            "name": "limit",
            "schema": {
              "type": "integer",
              "example": 50
            },
            "required": false
          }
        ],
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/autopilot/{strategyId}/{action}": {
      "post": {
        "operationId": "autopilot_action",
        "tags": [
          "autopilot"
        ],
        "summary": "Toggle / pause an autopilot strategy",
        "description": "Enable, disable, pause, or resume one strategy. Fund-moving toggles are gated on the standard two-step confirm workflow on the agent side.",
        "parameters": [
          {
            "in": "path",
            "name": "strategyId",
            "schema": {
              "type": "string"
            },
            "required": true
          },
          {
            "in": "path",
            "name": "action",
            "schema": {
              "type": "string",
              "enum": [
                "enable",
                "disable",
                "pause",
                "resume"
              ]
            },
            "required": true
          }
        ],
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/autopilot/managed/catalog": {
      "get": {
        "operationId": "autopilot_managed_catalog",
        "tags": [
          "autopilot"
        ],
        "summary": "Managed strategy catalog",
        "description": "List the managed (fully-managed) strategy types the picker can offer — Sharpe Guard and Futures Grid — each with default config and a backtest summary. `source` tells the UI how to render: `static-fallback` while the upstream catalog is pending, `upstream` once live, `schema-drift` when an upstream payload can't be parsed.",
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "catalog": [
                    {
                      "strategyType": "sharpe-guard",
                      "name": "Sharpe Guard",
                      "tags": [],
                      "description": "…",
                      "defaultConfig": {},
                      "backtestSummary": {
                        "estAprPct": 300.58,
                        "maxDrawdownPct": -38.9
                      }
                    }
                  ],
                  "source": "static-fallback"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/autopilot/managed/catalog/{strategyType}/backtest": {
      "get": {
        "operationId": "autopilot_managed_backtest",
        "tags": [
          "autopilot"
        ],
        "summary": "Managed strategy backtest",
        "description": "Backtest preview for one managed strategy type (equity curve + headline stats). Returns `{ available: false, reason }` when the current environment doesn't expose a backtest for that type.",
        "parameters": [
          {
            "in": "path",
            "name": "strategyType",
            "schema": {
              "type": "string",
              "enum": [
                "sharpe-guard",
                "futures-grid"
              ]
            },
            "required": true
          },
          {
            "in": "query",
            "name": "symbol",
            "schema": {
              "type": "string",
              "example": "BTC"
            },
            "required": false
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "available": true,
                  "equityCurve": [
                    {
                      "t": 1779900000000,
                      "v": 100
                    }
                  ],
                  "stats": {
                    "totalReturnPct": 300.58,
                    "sharpe": 2.1,
                    "maxDrawdownPct": -38.9,
                    "winRatePct": 61,
                    "totalTrades": 420,
                    "profitFactor": 1.8
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/autopilot/managed/strategies": {
      "get": {
        "operationId": "autopilot_managed_strategies",
        "tags": [
          "autopilot"
        ],
        "summary": "Managed strategies (flat list)",
        "description": "Every managed strategy with its bound sub-account and normalized status. The /autopilot page derives a sub-account → status map from this single fetch so each wallet row can show Running / Idle without an N+1 per-row poll.",
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "strategies": [
                    {
                      "strategyId": "str_…",
                      "subAccountId": "sub_…",
                      "strategyType": "sharpe-guard",
                      "name": "Sharpe Guard",
                      "symbols": [
                        "BTC"
                      ],
                      "status": "running"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      },
      "post": {
        "operationId": "autopilot_managed_create",
        "tags": [
          "autopilot"
        ],
        "summary": "Create a managed strategy (two-step confirm)",
        "description": "Create a managed strategy on a sub-wallet. Fund-moving: omit `confirm` (or pass false) to get `{ confirmed: false, preview }`; pass `confirm: true` with the same `idempotencyKey` to actually create. 409 when the sub-wallet already has a strategy or a create is already in flight. The strategy starts DISABLED — enabling autonomous trading is a separate manual step routed through chat.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              },
              "example": {
                "subAccountId": "sub_…",
                "strategyType": "sharpe-guard",
                "symbols": [
                  "BTC"
                ],
                "strategyConfig": {},
                "confirm": false,
                "idempotencyKey": "<uuid-v4>"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "confirmed": false,
                  "preview": {
                    "subAccountId": "sub_…",
                    "strategyType": "sharpe-guard",
                    "symbols": [
                      "BTC"
                    ],
                    "warning": "…"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/autopilot/managed/aggregated-summary": {
      "get": {
        "operationId": "autopilot_managed_aggregated_summary",
        "tags": [
          "autopilot"
        ],
        "summary": "Managed autopilot aggregated summary",
        "description": "Single aggregate source shared by the /autopilot KPI bar and the Dashboard managed card: total equity, total unrealized PnL, running vs total counts, and a trailing 30-day return sparkline. Fanned out once and cached 60s server-side to avoid an N+1 across the two consumers.",
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "totalEquity": "1234.56",
                  "totalUnrealizedPnl": "12.34",
                  "runningCount": 2,
                  "totalCount": 3,
                  "return30D": {
                    "pct": 12.5,
                    "sparkline": [
                      {
                        "t": 1779900000000,
                        "v": 0
                      }
                    ]
                  },
                  "lastUpdatedAt": 1779986400000,
                  "connected": true
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/autopilot/managed/xstrategy/deployments": {
      "get": {
        "operationId": "autopilot_managed_xstrategy_deployments",
        "tags": [
          "autopilot"
        ],
        "summary": "The caller's XStrategy deployments (picker 'My' tab)",
        "description": "Lists the user's live XStrategy deployments (any status) so the Autopilot strategy picker's 'My' tab can show them next to their Strategy Studio strategies. Read-only; upstream is untyped so the gateway projects each row defensively.",
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "deployments": [
                    {
                      "deploymentId": "dep_…",
                      "strategyId": "str_…",
                      "name": "…",
                      "status": "RUNNING",
                      "symbols": [
                        "BTC"
                      ],
                      "subAccountId": "sub_…",
                      "backtestId": "bt_…"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/autopilot/managed/xstrategy/backtest/{backtestId}": {
      "get": {
        "operationId": "autopilot_managed_xstrategy_backtest",
        "tags": [
          "autopilot"
        ],
        "summary": "A deployed XStrategy's backtest report (picker 'My' detail)",
        "description": "Full multi-asset backtest report for a deployment (add `/curves` for the equity curve). Namespaced under autopilot so the picker detail doesn't depend on the Top Strategies surface; reads the same upstream `/xstrategy/backtests/:id`.",
        "parameters": [
          {
            "in": "path",
            "name": "backtestId",
            "schema": {
              "type": "string"
            },
            "required": true
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "backtestId": "bt_…",
                  "netPnlPercent": 458.98,
                  "annualizedReturnPercent": 99.72,
                  "maxEquityDrawdownPercent": -26.27,
                  "sharpeRatio": 1.981,
                  "totalTrades": 2847,
                  "symbols": [
                    "NVDA",
                    "TSLA",
                    "MSFT"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/autopilot/managed/by-sub-account/{subAccountId}": {
      "get": {
        "operationId": "autopilot_managed_by_sub_account",
        "tags": [
          "autopilot"
        ],
        "summary": "Managed autopilot status for one sub-wallet",
        "description": "Per-sub-account snapshot the detail panel consumes: the bound strategy (or null), an account summary (equity / available / unrealized PnL), and a stale-data envelope (`connected` + `reason`) so the UI can show a degraded banner without clearing data.",
        "parameters": [
          {
            "in": "path",
            "name": "subAccountId",
            "schema": {
              "type": "string"
            },
            "required": true
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "subAccountId": "sub_…",
                  "strategy": null,
                  "summary": null,
                  "lastUpdatedAt": 1779986400000,
                  "connected": true
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/autopilot/managed/run": {
      "post": {
        "operationId": "autopilot_managed_run",
        "tags": [
          "autopilot"
        ],
        "summary": "Run a marketplace or studio strategy as autopilot (two-step confirm)",
        "description": "Run a strategy from another source as autopilot. `marketplace` subscribes the published strategy to the chosen wallet and starts it; `studio` starts an already-deployed strategy on its own wallet. Fund-moving: omit `confirm` (or pass false) for `{ confirmed: false, preview }`; pass `confirm: true` with the same `idempotencyKey` to execute. The built-in catalog keeps the `/strategies` create path.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              },
              "example": {
                "source": "marketplace",
                "strategyRef": "pub_…",
                "subAccountId": "sub_…",
                "settings": {},
                "confirm": false,
                "idempotencyKey": "<uuid-v4>"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "confirmed": false,
                  "preview": {
                    "source": "marketplace",
                    "strategyRef": "pub_…",
                    "subAccountId": "sub_…",
                    "warning": "…"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/autopilot/managed/{strategyId}/disable": {
      "post": {
        "operationId": "autopilot_managed_disable",
        "tags": [
          "autopilot"
        ],
        "summary": "Disable (stop) a managed strategy (two-step confirm)",
        "description": "Stop a running managed strategy. Fund-affecting, so it follows the same preview → confirm two-step as create. Stopping does not close open positions or cancel resting orders — the preview warning says so.",
        "parameters": [
          {
            "in": "path",
            "name": "strategyId",
            "schema": {
              "type": "string"
            },
            "required": true
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              },
              "example": {
                "confirm": false,
                "idempotencyKey": "<uuid-v4>"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "confirmed": false,
                  "preview": {
                    "strategyId": "str_…",
                    "subAccountId": "sub_…",
                    "warning": "…"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/autopilot/managed/xstrategy/{strategyId}/stop": {
      "post": {
        "operationId": "autopilot_managed_xstrategy_stop",
        "tags": [
          "autopilot"
        ],
        "summary": "Stop a self-authored XStrategy deployment (two-step confirm)",
        "description": "Stop a live XStrategy deployment the user authored (Autopilot surfaces it alongside managed + Strategy Studio strategies). Keyed by strategy id. Fund-affecting, so it follows the same preview → confirm two-step as disable; stopping does not close open positions.",
        "parameters": [
          {
            "in": "path",
            "name": "strategyId",
            "schema": {
              "type": "string"
            },
            "required": true
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              },
              "example": {
                "confirm": false,
                "idempotencyKey": "<uuid-v4>"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "confirmed": false,
                  "preview": {
                    "strategyId": "str_…",
                    "warning": "…"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/autopilot/managed/xstrategy/{strategyId}/deploy": {
      "post": {
        "operationId": "autopilot_managed_xstrategy_deploy",
        "tags": [
          "autopilot"
        ],
        "summary": "Run a self-authored XStrategy on a wallet (two-step confirm)",
        "description": "Deploy an XStrategy onto the selected sub-wallet by cloning the existing deployment's config (version / universe / interval / policy) and re-binding the wallet. Fund-moving, two-step preview → confirm. One active deployment per strategy — an already-running strategy returns a conflict.",
        "parameters": [
          {
            "in": "path",
            "name": "strategyId",
            "schema": {
              "type": "string"
            },
            "required": true
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              },
              "example": {
                "deploymentId": "dep_…",
                "subAccountId": "sub_…",
                "confirm": false,
                "idempotencyKey": "<uuid-v4>"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "confirmed": false,
                  "preview": {
                    "strategyId": "str_…",
                    "subAccountId": "sub_…",
                    "warning": "…"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/perps/transfer": {
      "post": {
        "operationId": "perps_transfer",
        "tags": [
          "perps"
        ],
        "summary": "Transfer USDC between perp sub-wallets (two-step confirm)",
        "description": "Move idle USDC from one perp sub-wallet to another. USDC only. Fund-moving: preview → confirm two-step keyed by `idempotencyKey`. 409 `insufficient_balance` when the amount exceeds the source wallet's idle USDC; 400 on a self-transfer.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              },
              "example": {
                "fromSubAccountId": "sub_a",
                "toSubAccountId": "sub_b",
                "amount": "100",
                "asset": "USDC",
                "confirm": false,
                "idempotencyKey": "<uuid-v4>"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "confirmed": false,
                  "preview": {
                    "fromSubAccountId": "sub_a",
                    "toSubAccountId": "sub_b",
                    "amount": "100",
                    "asset": "USDC",
                    "availableBefore": "126.71",
                    "warning": "…"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/perps/sub-account/{subAccountId}/idle-usdc": {
      "get": {
        "operationId": "perps_idle_usdc",
        "tags": [
          "perps"
        ],
        "summary": "Idle USDC available to transfer",
        "description": "The single 'available to transfer' number for a perp sub-wallet: idle USDC after subtracting margin in open positions, locked open-order balance, and venue reserve. Backs the Transfer dialog's Max button.",
        "parameters": [
          {
            "in": "path",
            "name": "subAccountId",
            "schema": {
              "type": "string"
            },
            "required": true
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "subAccountId": "sub_…",
                  "idleUsdc": "126.71",
                  "lastUpdatedAt": 1779986400000
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/profile": {
      "get": {
        "operationId": "profile",
        "tags": [
          "personalization"
        ],
        "summary": "Personalization snapshot",
        "description": "Read the rebuilt personalization snapshot the agent injects into every system prompt: trading summary text, behavioural tags, personalization-class memories, custom instructions, and visibility settings. Mirrors the REPL `/profile` command.",
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/profile/onboarding": {
      "get": {
        "operationId": "profile_onboarding_get",
        "tags": [
          "personalization"
        ],
        "summary": "Onboarding status",
        "description": "Whether the user has completed onboarding, plus their raw saved answers. Used by the web UI to decide whether to show the onboarding flow.",
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "ok": true,
                  "data": {
                    "hasCompleted": true,
                    "responses": {
                      "financeKnowledge": "level_2",
                      "risk": "balanced",
                      "markets": [
                        "crypto_majors",
                        "stocks"
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      },
      "post": {
        "operationId": "profile_onboarding_submit",
        "tags": [
          "personalization"
        ],
        "summary": "Submit onboarding answers",
        "description": "Apply onboarding answers: maps finance knowledge / frequency / risk / markets to user-sourced behavioural tags, writes one personalization memory per answered dimension (first completion only), and records the self-reported capital as a memory (never a tag). Idempotent.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              },
              "example": {
                "financeKnowledge": "level_2",
                "frequency": "weekly",
                "risk": "balanced",
                "markets": [
                  "crypto_majors",
                  "stocks"
                ],
                "capital": "$1k-5k"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "ok": true,
                  "data": {
                    "hasCompleted": true,
                    "tagsSet": [
                      "finance_knowledge",
                      "frequency",
                      "risk",
                      "markets"
                    ],
                    "memoriesWritten": 5
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/financial-profile": {
      "get": {
        "operationId": "financial_profile_snapshot",
        "tags": [
          "personalization"
        ],
        "summary": "Financial profile snapshot",
        "description": "Latest persisted `financial_profile` row: rebuilt platform wallet summary (composite of in-session trades + perps fills + spot activities), reference wallets, custom prompt, visibility flags, and rebuild cursors.",
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/profile/prompt": {
      "put": {
        "operationId": "profile_prompt_set",
        "tags": [
          "personalization"
        ],
        "summary": "Set custom prompt",
        "description": "Replace the user's custom system-prompt addendum (max 2000 chars). Surfaces in the personalization snapshot on the next agent turn.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              },
              "example": {
                "text": "Always quote prices in CNY alongside USD."
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "ok": true,
                  "data": {
                    "custom_prompt": "Always quote prices in CNY alongside USD."
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      },
      "delete": {
        "operationId": "profile_prompt_clear",
        "tags": [
          "personalization"
        ],
        "summary": "Clear custom prompt",
        "description": "Drop the addendum. UI surfaces a hard-confirm before calling.",
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "ok": true,
                  "data": {
                    "custom_prompt": null
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/profile/tags/{name}": {
      "put": {
        "operationId": "profile_tag_set",
        "tags": [
          "personalization"
        ],
        "summary": "Set a behavioural tag",
        "description": "Set or clear one of the 10 behavioural-tag dimensions (e.g. `risk`, `markets`, `FOMO Index`). Pass a string for single-value dimensions; pass an array for the multi-select `markets` dimension. `value: null` clears the tag — the agent re-infers it from chat + trade history on the next rebuild.",
        "parameters": [
          {
            "in": "path",
            "name": "name",
            "schema": {
              "type": "string",
              "example": "risk"
            },
            "required": true
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              },
              "example": {
                "value": "balanced"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "ok": true,
                  "data": {
                    "name": "risk",
                    "value": "balanced",
                    "source": "user",
                    "updated_at": "..."
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      },
      "delete": {
        "operationId": "profile_tag_delete",
        "tags": [
          "personalization"
        ],
        "summary": "Reset a behavioural tag",
        "description": "Equivalent to PUT with `value: null` — the tag is cleared and the agent re-infers it from history.",
        "parameters": [
          {
            "in": "path",
            "name": "name",
            "schema": {
              "type": "string"
            },
            "required": true
          }
        ],
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/memory": {
      "get": {
        "operationId": "memory_list",
        "tags": [
          "personalization"
        ],
        "summary": "List or search memories",
        "description": "Read recent memories filtered by `category`, or run a hybrid (BM25 + vector) search via `query`. Soft-deleted rows (`deleted_at IS NOT NULL`) are excluded.",
        "parameters": [
          {
            "in": "query",
            "name": "category",
            "schema": {
              "type": "string",
              "example": "personalization"
            },
            "required": false
          },
          {
            "in": "query",
            "name": "query",
            "schema": {
              "type": "string"
            },
            "required": false
          },
          {
            "in": "query",
            "name": "limit",
            "schema": {
              "type": "integer",
              "example": 50
            },
            "required": false
          }
        ],
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      },
      "post": {
        "operationId": "memory_create",
        "tags": [
          "personalization"
        ],
        "summary": "Create a user memory",
        "description": "Hand-write a memory the agent should always remember. Server stamps `category=personalization` + `source=user_manual`; `fact_type` and `tickers` are stored in metadata.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              },
              "example": {
                "statement": "I do not trade meme coins.",
                "fact_type": "constraint.hard",
                "tickers": []
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "ok": true,
                  "id": 142
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/memory/trading-cases": {
      "get": {
        "operationId": "memory_trading_cases",
        "tags": [
          "personalization"
        ],
        "summary": "List free-form trading-case notes",
        "description": "Legacy free-form notes feed (memories table where `category = 'trading-cases'`). For the methodology audit feed, see `/v1/memory/methodology-cases`.",
        "parameters": [
          {
            "in": "query",
            "name": "ticker",
            "schema": {
              "type": "string"
            },
            "required": false
          },
          {
            "in": "query",
            "name": "limit",
            "schema": {
              "type": "integer",
              "example": 50
            },
            "required": false
          }
        ],
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/memory/research-cases": {
      "get": {
        "operationId": "memory_research_cases",
        "tags": [
          "personalization"
        ],
        "summary": "List research-case notes",
        "description": "Memories from `/research` runs filtered by `topic`. Used by the Research retrospect cards.",
        "parameters": [
          {
            "in": "query",
            "name": "topic",
            "schema": {
              "type": "string"
            },
            "required": false
          },
          {
            "in": "query",
            "name": "limit",
            "schema": {
              "type": "integer",
              "example": 50
            },
            "required": false
          }
        ],
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/memory/{id}": {
      "patch": {
        "operationId": "memory_update",
        "tags": [
          "personalization"
        ],
        "summary": "Edit a user memory",
        "description": "Edit `statement`, `fact_type`, or `tickers` on a `source=user_manual` memory. Other source values return 403 — agent-authored facts are immutable from the gateway.",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "schema": {
              "type": "integer"
            },
            "required": true
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              },
              "example": {
                "statement": "I do not trade pre-launch meme coins."
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "ok": true,
                  "updated": true
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      },
      "delete": {
        "operationId": "memory_delete",
        "tags": [
          "personalization"
        ],
        "summary": "Soft-delete a user memory",
        "description": "Mark `deleted_at = now()`. Read paths (FTS, listings, snapshot) filter the row out. A 30-day retention cron physically deletes it later. Restore via `POST /v1/memory/:id/restore`.",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "schema": {
              "type": "integer"
            },
            "required": true
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "ok": true,
                  "deleted": true
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/memory/{id}/restore": {
      "post": {
        "operationId": "memory_restore",
        "tags": [
          "personalization"
        ],
        "summary": "Restore a soft-deleted memory",
        "description": "Clears `deleted_at`. Wired to the web-UI undo toast (5 second window) but works at any time before the 30-day purge.",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "schema": {
              "type": "integer"
            },
            "required": true
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "ok": true,
                  "restored": true
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/profile/refresh": {
      "post": {
        "operationId": "profile_refresh",
        "tags": [
          "personalization"
        ],
        "summary": "Force a personalization rebuild",
        "description": "Run all three personalization rebuilds (trading_summary / tags / memories) with `force=true`. Also kicks off `MinaraHistorySync.runOnce()` first so external perps + spot history is current before the rebuild reads it. Throttle gates do not apply on the force path.",
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "ok": true,
                  "results": {
                    "trading_summary": {
                      "changed": true
                    },
                    "tags": {},
                    "memories": {}
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/profile/trade-history-breakdown": {
      "get": {
        "operationId": "profile_trade_history_breakdown",
        "tags": [
          "personalization"
        ],
        "summary": "Trade history breakdown",
        "description": "Three-source breakdown feeding the Financial Profile dashboard: local trade_history count, recent perps fills + per-symbol aggregate, recent spot activities + per-pair aggregate, last sync timestamp.",
        "parameters": [
          {
            "in": "query",
            "name": "perps_limit",
            "schema": {
              "type": "integer",
              "example": 30
            },
            "required": false
          },
          {
            "in": "query",
            "name": "spot_limit",
            "schema": {
              "type": "integer",
              "example": 20
            },
            "required": false
          },
          {
            "in": "query",
            "name": "window_days",
            "schema": {
              "type": "integer",
              "example": 90
            },
            "required": false
          }
        ],
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/profile/reference-wallets": {
      "get": {
        "operationId": "profile_reference_wallets_get",
        "tags": [
          "personalization"
        ],
        "summary": "List reference wallets",
        "description": "User's watchlist of external wallet addresses.",
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "wallets": [
                    "0xabc...",
                    "9XYZ..."
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      },
      "put": {
        "operationId": "profile_reference_wallets_set",
        "tags": [
          "personalization"
        ],
        "summary": "Replace reference wallet list",
        "description": "Replace the watchlist atomically. Server validates EVM (`0x` + 40 hex) and Solana / generic base58 (32-44 chars) addresses; rejects anything else.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              },
              "example": {
                "wallets": [
                  "0xabc...",
                  "9XYZ..."
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "ok": true,
                  "wallets": [
                    "0xabc...",
                    "9XYZ..."
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/memory/trading-cases/stats": {
      "get": {
        "operationId": "trading_cases_stats",
        "tags": [
          "learning"
        ],
        "summary": "Methodology cases snapshot",
        "description": "4-number snapshot for the Trading-cases dashboard: total cases, last 30 days, winning ratio, avg alpha, top methodology id.",
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/memory/trading-cases/methodology-trend": {
      "get": {
        "operationId": "methodology_trend",
        "tags": [
          "learning"
        ],
        "summary": "Top-N methodology alpha trend",
        "description": "Per-week timeseries of `outcome_alpha_return` for the top-N methodologies over the requested window. Powers the SVG line chart on the Trading-cases page.",
        "parameters": [
          {
            "in": "query",
            "name": "weeks",
            "schema": {
              "type": "integer",
              "example": 12
            },
            "required": false
          },
          {
            "in": "query",
            "name": "top",
            "schema": {
              "type": "integer",
              "example": 5
            },
            "required": false
          }
        ],
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/memory/methodologies": {
      "get": {
        "operationId": "methodologies_list",
        "tags": [
          "learning"
        ],
        "summary": "Top learned methodologies",
        "description": "Top-N methodologies ranked by Wilson-lower confidence, with case count, asset classes, last-seen, and active / quarantined / insufficient_data status. Drives the 'Top patterns' cards.",
        "parameters": [
          {
            "in": "query",
            "name": "limit",
            "schema": {
              "type": "integer",
              "example": 8
            },
            "required": false
          }
        ],
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/memory/methodology-cases": {
      "get": {
        "operationId": "methodology_cases_list",
        "tags": [
          "learning"
        ],
        "summary": "Raw methodology cases ledger",
        "description": "Read the `methodology_cases` table directly. Powers the audit ledger drawer; row ids share the same id space as `/v1/memory/trading-cases/:id`.",
        "parameters": [
          {
            "in": "query",
            "name": "limit",
            "schema": {
              "type": "integer",
              "example": 200
            },
            "required": false
          }
        ],
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/memory/trading-cases/{id}": {
      "get": {
        "operationId": "trading_case_detail",
        "tags": [
          "learning"
        ],
        "summary": "Single methodology case",
        "description": "Full row JSON for one `methodology_cases.id`. Used by the right-side Drawer in the Trading-cases page.",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "schema": {
              "type": "integer"
            },
            "required": true
          }
        ],
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/learned-preferences": {
      "get": {
        "operationId": "learned_preferences_list",
        "tags": [
          "preferences"
        ],
        "summary": "List learned preferences",
        "description": "Inferred behavioural preferences awaiting your call (proposed / active / deprecated). Drives the Personalization page's 'Learned preferences' card.",
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/learned-preferences/{id}/{action}": {
      "post": {
        "operationId": "learned_preferences_action",
        "tags": [
          "preferences"
        ],
        "summary": "Approve / reject / deprecate a preference",
        "description": "Transition one learned preference. Approve makes it active and brings it into the next agent system prompt; reject or deprecate hides it.",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "schema": {
              "type": "string"
            },
            "required": true
          },
          {
            "in": "path",
            "name": "action",
            "schema": {
              "type": "string",
              "enum": [
                "approve",
                "reject",
                "deprecate"
              ]
            },
            "required": true
          }
        ],
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/learned-preferences/{id}": {
      "get": {
        "operationId": "learned_preference_detail",
        "tags": [
          "preferences"
        ],
        "summary": "Get one learned preference",
        "description": "Full record for one learned preference id (description, signal strength, history).",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "schema": {
              "type": "string"
            },
            "required": true
          }
        ],
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/watchlist": {
      "get": {
        "operationId": "watchlist_get",
        "tags": [
          "preferences"
        ],
        "summary": "Get web-UI watchlist",
        "description": "User-curated symbol watchlist persisted on the profile.",
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      },
      "put": {
        "operationId": "watchlist_set",
        "tags": [
          "preferences"
        ],
        "summary": "Replace web-UI watchlist",
        "description": "Replace the watchlist (max 100 symbols).",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              },
              "example": {
                "symbols": [
                  {
                    "ticker": "BTC",
                    "kind": "crypto"
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/runtime-preferences": {
      "get": {
        "operationId": "runtime_preferences_get",
        "tags": [
          "preferences"
        ],
        "summary": "Get runtime preferences",
        "description": "Returns the sub-feature toggle schema, current overrides, and the resolved values. Backs the Settings → Preferences page.",
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      },
      "put": {
        "operationId": "runtime_preferences_put",
        "tags": [
          "preferences"
        ],
        "summary": "Apply runtime preference overrides",
        "description": "Batch apply user overrides with all-or-nothing validation. Supports If-Match concurrency via header or body. Critical-tier toggles require a signed ack token.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              },
              "example": {
                "if_match": "<rev>",
                "overrides": {
                  "thinking.enabled": true
                },
                "ack_token": "<optional>"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/runtime-preferences/critical-unlock-status": {
      "get": {
        "operationId": "runtime_preferences_critical_unlock_status",
        "tags": [
          "preferences"
        ],
        "summary": "Critical-unlock state",
        "description": "Read whether critical-tier toggles are currently unlocked (via the CLI `minara settings unlock-critical` or POST /v1/runtime-preferences/critical-unlock).",
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/runtime-preferences/critical-unlock": {
      "post": {
        "operationId": "runtime_preferences_critical_unlock",
        "tags": [
          "preferences"
        ],
        "summary": "Issue a critical-unlock",
        "description": "Issue a short-lived unlock that lets a subsequent PUT /v1/runtime-preferences flip a critical-tier toggle into its danger direction. Optional `ttlMs` body sets the lifetime (default 5 minutes). The typed-confirm ack on the PUT is still required.",
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/runtime-preferences/sign-ack": {
      "post": {
        "operationId": "runtime_preferences_sign_ack",
        "tags": [
          "preferences"
        ],
        "summary": "Server-signed ack token for critical confirms",
        "description": "Mint a short-lived signed ack token consumed by the next PUT /v1/runtime-preferences call that touches a critical-tier toggle.",
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/runtime-preferences/events": {
      "get": {
        "operationId": "runtime_preferences_events",
        "tags": [
          "preferences"
        ],
        "summary": "SSE preferences events",
        "description": "Server-sent stream emitting `saved { revision, lifecycleEffects }` whenever runtime preferences change. Used for cross-tab sync in the Settings page.",
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/notifications": {
      "get": {
        "operationId": "notifications_list",
        "tags": [
          "notifications"
        ],
        "summary": "List notifications",
        "description": "Recent in-app notifications (newest first) plus an `unread_count`. A notification is recorded each time a custom agent fires on its own (scheduled / event / once) and its in-app config opts into the outcome. Optional `?limit=` (default 50).",
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      },
      "delete": {
        "operationId": "notifications_clear",
        "tags": [
          "notifications"
        ],
        "summary": "Clear all notifications",
        "description": "Delete every notification. Returns `{ ok, cleared }`.",
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/notifications/stream": {
      "get": {
        "operationId": "notifications_stream",
        "tags": [
          "notifications"
        ],
        "summary": "SSE notification stream",
        "description": "Server-sent stream emitting a `notification` event whenever a new notification is recorded. Powers the live toast + unread badge. Accepts `?token=` for EventSource (which can't set an Authorization header).",
        "responses": {
          "200": {
            "description": "Server-Sent Events stream",
            "content": {
              "text/event-stream": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/notifications/read-all": {
      "post": {
        "operationId": "notifications_read_all",
        "tags": [
          "notifications"
        ],
        "summary": "Mark all read",
        "description": "Mark every unread notification as read. Returns `{ ok, updated }`.",
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/notifications/{id}/read": {
      "post": {
        "operationId": "notifications_read",
        "tags": [
          "notifications"
        ],
        "summary": "Mark one read",
        "description": "Mark a single notification read (idempotent). Returns `{ ok: true, updated }` — `updated` is false if it was already read. 404 only if the id is missing.",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "schema": {
              "type": "string"
            },
            "required": true
          }
        ],
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/notifications/{id}": {
      "delete": {
        "operationId": "notifications_dismiss",
        "tags": [
          "notifications"
        ],
        "summary": "Dismiss a notification",
        "description": "Delete a single notification by id.",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "schema": {
              "type": "string"
            },
            "required": true
          }
        ],
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/credentials/families": {
      "get": {
        "operationId": "credentials_families",
        "tags": [
          "preferences"
        ],
        "summary": "List credential families",
        "description": "Returns the provider-family registry (web search, market data, embeddings, cloud compute, etc.) along with each field's set / display / source status. Backs the Settings → API Keys page.",
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/credentials/families/{id}": {
      "put": {
        "operationId": "credentials_family_put",
        "tags": [
          "preferences"
        ],
        "summary": "Upsert credential family overrides",
        "description": "Writes overrides for a single provider family into `~/.minara/runtime-credentials.json` (atomic + file-locked). .env files are never touched.",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "schema": {
              "type": "string"
            },
            "required": true
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              },
              "example": {
                "fields": {
                  "COINGECKO_API_KEY": "cg-...",
                  "GLASSNODE_API_KEY": ""
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/credentials/families/{id}/field/{envVar}": {
      "delete": {
        "operationId": "credentials_family_field_delete",
        "tags": [
          "preferences"
        ],
        "summary": "Clear a credential override",
        "description": "Remove a single field's override (reverts to env-derived fallback).",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "schema": {
              "type": "string"
            },
            "required": true
          },
          {
            "in": "path",
            "name": "envVar",
            "schema": {
              "type": "string"
            },
            "required": true
          }
        ],
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/migrate/manifest": {
      "get": {
        "operationId": "migrate_manifest",
        "tags": [
          "migrate"
        ],
        "summary": "Export manifest",
        "description": "List the data sets that can be exported (memories, profile, prefs, workflows). Pre-flight for `/v1/migrate/export`.",
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/migrate/export": {
      "post": {
        "operationId": "migrate_export",
        "tags": [
          "migrate"
        ],
        "summary": "Export user data",
        "description": "Bundle the requested manifest entries into a portable JSON blob for backup / migration to another deployment.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              },
              "example": {
                "include": [
                  "memories",
                  "profile",
                  "preferences"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/migrate/import": {
      "post": {
        "operationId": "migrate_import",
        "tags": [
          "migrate"
        ],
        "summary": "Import user data",
        "description": "Restore from a previously exported bundle. Idempotent on stable ids (memories, preferences); replaces watchlist + profile.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              },
              "example": "{ \"bundle\": { /* output of /v1/migrate/export */ } }"
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/auth/minara/oauth/start": {
      "post": {
        "operationId": "auth_minara_oauth_start",
        "tags": [
          "auth"
        ],
        "summary": "Begin Minara OAuth PKCE flow",
        "description": "Kick off the Minara platform OAuth 2.0 + PKCE login. Binds a loopback callback server, returns the authorize URL the user opens in a browser. On consent, the agent persists a minara-oauth profile.",
        "responses": {
          "200": {
            "description": "Success"
          }
        },
        "security": []
      }
    },
    "/v1/auth/minara/oauth/poll": {
      "post": {
        "operationId": "auth_minara_oauth_poll",
        "tags": [
          "auth"
        ],
        "summary": "Poll Minara OAuth flow status",
        "description": "Poll the in-flight OAuth flow by flow_id. Returns pending until the callback fires, then completed (profile saved) or error / expired.",
        "responses": {
          "200": {
            "description": "Success"
          }
        },
        "security": []
      }
    },
    "/v1/auth/oauth/minara/init": {
      "post": {
        "operationId": "auth_oauth_minara_init",
        "tags": [
          "auth"
        ],
        "summary": "Begin Minara reauthorization (shared provider flow)",
        "description": "Alias of the Minara OAuth start under the shared provider-reauth namespace. Runs the same PKCE flow and returns the authorize URL and flow id in the camelCase shape the provider-reauth UI expects. Completion is polled via /v1/auth/oauth/flows/:state.",
        "responses": {
          "200": {
            "description": "Success"
          }
        },
        "security": []
      }
    },
    "/v1/auth/minara": {
      "delete": {
        "operationId": "auth_minara_clear",
        "tags": [
          "auth"
        ],
        "summary": "Disconnect Minara",
        "description": "Forget the stored Minara token; subsequent calls become unauthenticated.",
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/gateway/providers": {
      "get": {
        "operationId": "gateway_providers",
        "tags": [
          "gateway"
        ],
        "summary": "Messaging providers",
        "description": "Inbound messaging providers registered with the gateway (Slack, Lark, etc).",
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/gateway/providers/{id}/fields/{envVar}/reveal": {
      "post": {
        "operationId": "gateway_provider_field_reveal",
        "tags": [
          "gateway"
        ],
        "summary": "Reveal one credential field",
        "description": "Return the plaintext value of a single masked credential field (e.g. `TELEGRAM_BOT_TOKEN`) for the Settings → Messaging \"Show\" button. The implicit `GET /v1/gateway/providers` payload always masks; this endpoint narrowly widens to plaintext on explicit user request, one field at a time. Auth: same-origin / loopback / missing-Origin requests pass through (the single-user local-dev case); cross-origin callers must present the gateway bearer token or receive 503 `gateway_auth_required`. Non-masked, unknown, or unset fields return 404.",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "schema": {
              "type": "string"
            },
            "required": true
          },
          {
            "in": "path",
            "name": "envVar",
            "schema": {
              "type": "string"
            },
            "required": true
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "value": "85123abcdef..."
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/gateway/providers/{id}/oauth/init": {
      "post": {
        "operationId": "gateway_provider_oauth_init",
        "tags": [
          "gateway"
        ],
        "summary": "Start Gmail OAuth connect",
        "description": "Begin a Google OAuth consent flow for an OAuth-style messaging provider (`email-gmail`). The gateway binds a loopback callback, returns the Google `authorizeUrl` for the browser to open, then on the callback exchanges the code and persists the connection (`GMAIL_REFRESH_TOKEN` + `GMAIL_SENDER_EMAIL`). The web UI polls `GET /v1/auth/oauth/flows/:state` until done. Returns 400 `google_client_not_configured` when the operator's Google client id/secret are unset, 404 `provider_not_oauth` for non-OAuth providers, and 409 `port_in_use` when the callback port is busy.",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "schema": {
              "type": "string"
            },
            "required": true
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "authorizeUrl": "https://accounts.google.com/o/oauth2/v2/auth?...",
                  "state": "...",
                  "flowId": "...",
                  "redirectUri": "http://127.0.0.1:1457/callback",
                  "expiresAt": 1730000000000
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/llm/default-model": {
      "get": {
        "operationId": "llm_default_model_get",
        "tags": [
          "llm"
        ],
        "summary": "Get default agent model",
        "description": "The model id the agent loop currently uses for new turns.",
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      },
      "put": {
        "operationId": "llm_default_model_set",
        "tags": [
          "llm"
        ],
        "summary": "Set default agent model",
        "description": "Switch the agent loop to a different model id. Validated against the live `available-models` list.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              },
              "example": {
                "model": "claude-opus-4-7"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/llm/available-models": {
      "get": {
        "operationId": "llm_available_models",
        "tags": [
          "llm"
        ],
        "summary": "Available models",
        "description": "Models the gateway can dispatch to, grouped by provider. Reflects the user's active auth profiles.",
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/llm/available-models/refresh": {
      "post": {
        "operationId": "llm_available_models_refresh",
        "tags": [
          "llm"
        ],
        "summary": "Refresh available models",
        "description": "Force a live re-fetch of the active provider's model catalog, bypassing the in-memory and 24h disk caches, then return the refreshed list. Lets the UI surface a model released after the agent booted without a restart.",
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "provider": "anthropic-oauth",
                  "current": "claude-opus-4-7",
                  "models": [
                    {
                      "id": "claude-opus-4-8",
                      "label": "Claude Opus 4.8"
                    }
                  ],
                  "source": "live"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/shortcut-questions": {
      "get": {
        "operationId": "shortcut_questions",
        "tags": [
          "llm"
        ],
        "summary": "Shortcut questions (Chat landing)",
        "description": "Hand-authored fixed lists plus LLM-generated dynamic questions per category, used by the Chat landing page chip-row. Categories: Trending, Crypto (research / trading / DeFi / airdrop / meme), Stock (research / trading), Macro, Sentiment, Learn. Dynamic questions are regenerated on a 6-hour cadence by default (SHORTCUT_QUESTIONS_TTL_MS).",
        "parameters": [
          {
            "in": "query",
            "name": "locale",
            "schema": {
              "type": "string",
              "example": "en"
            },
            "required": false
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "locale": "en",
                  "leaves": [
                    {
                      "topLevel": "trending",
                      "sub": null,
                      "fixed": [
                        "..."
                      ],
                      "dynamic": [
                        "What's pumping $BTC this week?"
                      ],
                      "generatedAt": 1746000000000
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/quote/batch": {
      "get": {
        "operationId": "quote_batch",
        "tags": [
          "market"
        ],
        "summary": "Batch ticker quotes",
        "description": "Spot/perp quotes for many tickers in one round trip — used by the Ticker Board to populate ~20 cells per tick.",
        "parameters": [
          {
            "in": "query",
            "name": "tickers",
            "schema": {
              "type": "string",
              "example": "BTC,ETH,SOL"
            },
            "required": true
          }
        ],
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/quote/{ticker}": {
      "get": {
        "operationId": "quote_single",
        "tags": [
          "market"
        ],
        "summary": "Single ticker quote",
        "description": "Spot price + 24h change + traditional metrics (P/E, EPS, dividend yield, 52-week high/low, beta, market cap) for one non-crypto ticker. Source chain: Minara `stocks/v2/get-stock-info` for price + fundamentals; Yahoo `quoteSummary` in parallel for the rich metrics. Falls back to Yahoo as primary for symbols Minara 404s (commodities, forex, indices).",
        "parameters": [
          {
            "in": "path",
            "name": "ticker",
            "schema": {
              "type": "string"
            },
            "required": true
          }
        ],
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/quote/crypto/{symbol}": {
      "get": {
        "operationId": "quote_crypto",
        "tags": [
          "market"
        ],
        "summary": "Single crypto token stats",
        "description": "Market cap, FDV, 24h volume (USD), 24h % change, name, logo, and chain for one crypto symbol. Sourced from Minara `searchTokens` by picking the first row whose symbol matches exactly (case-insensitive). Used by the markets-detail Crypto header. Returns `supported: false` when Minara has no exact-match row for the symbol.",
        "parameters": [
          {
            "in": "path",
            "name": "symbol",
            "schema": {
              "type": "string",
              "example": "BTC"
            },
            "required": true
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "supported": true,
                  "symbol": "BTC",
                  "name": "Bitcoin",
                  "chain": "btc",
                  "logo_url": "https://...",
                  "price": 78000.12,
                  "price_change_24h_pct": 1.23,
                  "volume_24h": 32500000000,
                  "market_cap": 1540000000000,
                  "fdv": 1640000000000
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/market/candles": {
      "get": {
        "operationId": "market_candles",
        "tags": [
          "market"
        ],
        "summary": "Candle (K-line) series",
        "description": "OHLC bars for one ticker + interval. Routes to the appropriate provider (Hyperliquid / Yahoo Finance) based on asset class.",
        "parameters": [
          {
            "in": "query",
            "name": "ticker",
            "schema": {
              "type": "string"
            },
            "required": true
          },
          {
            "in": "query",
            "name": "interval",
            "schema": {
              "type": "string",
              "example": "1h"
            },
            "required": false
          }
        ],
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/market/sparkline": {
      "get": {
        "operationId": "market_sparkline",
        "tags": [
          "market"
        ],
        "summary": "Sparkline series",
        "description": "Compact close-only series for inline sparkline rendering in the web UI.",
        "parameters": [
          {
            "in": "query",
            "name": "ticker",
            "schema": {
              "type": "string"
            },
            "required": true
          }
        ],
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/market/search": {
      "get": {
        "operationId": "market_search",
        "tags": [
          "market"
        ],
        "summary": "Multi-asset symbol search",
        "description": "Fuzzy-match ticker symbols + names across crypto, stocks, ETFs, commodities, forex, and indices. Powers the typeahead on the web UI's `/markets` page. Source chain: Minara token + stock search first; Yahoo Finance fills commodities, forex, indices, and long-tail equities. Pass `?source=minara` or `?source=yahoo` to fan one tier out independently for progressive UX rendering — the default `source=all` waits for both and returns the merged set.",
        "parameters": [
          {
            "in": "query",
            "name": "q",
            "schema": {
              "type": "string",
              "example": "bit"
            },
            "required": true,
            "description": "Search query (ticker or partial name). Max 64 chars."
          },
          {
            "in": "query",
            "name": "limit",
            "schema": {
              "type": "integer",
              "example": 10
            },
            "description": "Max results returned (default 10, max 50)."
          },
          {
            "in": "query",
            "name": "source",
            "schema": {
              "type": "string",
              "enum": [
                "all",
                "minara",
                "yahoo"
              ],
              "example": "minara"
            },
            "description": "Restrict to a single upstream tier. `all` (default) merges Minara + Yahoo on the server. `minara` returns only Minara token+stock hits (fast). `yahoo` returns only Yahoo Finance hits (covers commodities, forex, indices, long-tail equities). The web UI fires `minara` + `yahoo` in parallel for progressive rendering."
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "results": [
                    {
                      "symbol": "BTC",
                      "name": "Bitcoin",
                      "category": "crypto",
                      "exchange": "Binance",
                      "logoUrl": "https://...",
                      "source": "minara"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/markets/movers": {
      "get": {
        "operationId": "markets_movers",
        "tags": [
          "market"
        ],
        "summary": "Top gainers / losers / most active",
        "description": "Biggest movers for the markets overview board. `class=stocks` (default) returns equity gainers, losers, and most-active from Yahoo Finance's public predefined screeners, with Minara trending stocks as a fallback. `class=crypto` returns a Minara-trending fallback only — the board's primary crypto movers come from the Binance public WebSocket stream client-side. Cached for 60s.",
        "parameters": [
          {
            "in": "query",
            "name": "class",
            "schema": {
              "type": "string",
              "enum": [
                "stocks",
                "crypto"
              ],
              "example": "stocks"
            },
            "description": "Asset class: `stocks` (default) or `crypto`."
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "gainers": [
                    {
                      "symbol": "NVDA",
                      "name": "NVIDIA",
                      "price": 123.4,
                      "changePct": 5.2,
                      "volume": 1000000
                    }
                  ],
                  "losers": [],
                  "active": []
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/markets/snapshot": {
      "get": {
        "operationId": "markets_snapshot",
        "tags": [
          "market"
        ],
        "summary": "Macro market snapshot",
        "description": "The markets overview board's macro bundle in one call: major index quotes (with intraday sparklines), commodity futures, major forex pairs, treasury yields, and the crypto Fear & Greed reading. All values come from keyless public providers (Yahoo Finance quotes, alternative.me sentiment, Minara as the sentiment fallback). Each symbol degrades on its own, so a single failed fetch never empties the bundle. Cached for 30s.",
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "indices": [
                    {
                      "symbol": "^GSPC",
                      "label": "S&P 500",
                      "price": 5400,
                      "change": 12,
                      "changePct": 0.2,
                      "spark": [
                        5388,
                        5400
                      ]
                    }
                  ],
                  "futures": [],
                  "forex": [],
                  "treasury": [
                    {
                      "label": "10Y",
                      "yield": 4.45,
                      "change": -0.02
                    }
                  ],
                  "fearGreed": {
                    "value": 54,
                    "label": "Neutral"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/markets/sectors": {
      "get": {
        "operationId": "markets_sectors",
        "tags": [
          "market"
        ],
        "summary": "Sector performance heatmap",
        "description": "Daily performance of the 11 GICS sectors for the board's heatmap, proxied by the SPDR sector ETFs (keyless Yahoo Finance quotes). Each sector always appears, even when its quote fails, so the heatmap keeps all tiles. Cached for 5 minutes.",
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "sectors": [
                    {
                      "name": "Technology",
                      "changePct": 1.2
                    },
                    {
                      "name": "Energy",
                      "changePct": -0.8
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/markets/news": {
      "get": {
        "operationId": "markets_news",
        "tags": [
          "market"
        ],
        "summary": "Market news headlines",
        "description": "A market headline feed for the board, parsed from Yahoo Finance's keyless RSS. `class=general` (default) covers the broad market; `class=crypto` covers major coins. Best-effort: an unreachable feed returns an empty list. Cached for 5 minutes.",
        "parameters": [
          {
            "in": "query",
            "name": "class",
            "schema": {
              "type": "string",
              "enum": [
                "general",
                "crypto"
              ],
              "example": "general"
            },
            "description": "Feed: `general` (default) or `crypto`."
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "items": [
                    {
                      "title": "Stocks close higher",
                      "site": "Yahoo Finance",
                      "url": "https://...",
                      "publishedAt": "2026-06-20T20:00:00.000Z"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/markets/calendar": {
      "get": {
        "operationId": "markets_calendar",
        "tags": [
          "market"
        ],
        "summary": "Upcoming earnings",
        "description": "The next session's earnings reports from Nasdaq's keyless calendar, forward-filled past weekends so it's never blank. Cached for 15 minutes.",
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "earnings": [
                    {
                      "symbol": "NVDA",
                      "name": "NVIDIA",
                      "time": "time-after-hours",
                      "epsEstimate": 1.23
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/skills": {
      "get": {
        "operationId": "skills_list",
        "tags": [
          "skills"
        ],
        "summary": "List registered skills",
        "description": "Every registered domain skill the agent can activate. Each entry carries activation policy, risk tier, and a `source` of `builtin` or `external` (vendored SKILL.md packages added via the CLI).",
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "skills": [
                    {
                      "id": "minara.core",
                      "name": "...",
                      "description": "...",
                      "activation": "always",
                      "tags": [],
                      "tool_names": [
                        "..."
                      ],
                      "requires_env": [],
                      "env_satisfied": true,
                      "risk_tier": 2,
                      "source": "builtin"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/skills/reload": {
      "post": {
        "operationId": "skills_reload",
        "tags": [
          "skills"
        ],
        "summary": "Reload external skill packages",
        "description": "Re-mirror vendored external skill packages into the sandbox and re-register them on the in-process registry. The `minara skills add / remove / upgrade` CLI calls this on a running gateway so the change takes effect without restart.",
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "ok": true,
                  "added": [
                    "external.foo"
                  ],
                  "reregistered": [],
                  "removed": []
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/theme": {
      "get": {
        "operationId": "theme_get",
        "tags": [
          "theme"
        ],
        "summary": "Get user theme",
        "description": "Web-UI theme preference (light/dark/system + accent).",
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      },
      "post": {
        "operationId": "theme_set",
        "tags": [
          "theme"
        ],
        "summary": "Set user theme",
        "description": "Update the persisted theme preference.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              },
              "example": {
                "mode": "dark",
                "accent": "primary"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/admin/script-risk/decisions": {
      "get": {
        "operationId": "admin_script_risk_decisions",
        "tags": [
          "admin"
        ],
        "summary": "List script-risk decisions",
        "description": "Audit log of the script-risk gate's decisions in front of `execute_code` / `terminal` / `write_file` / `patch`. Shows accepted and rejected commands with the operator-visible reason.",
        "parameters": [
          {
            "in": "query",
            "name": "limit",
            "schema": {
              "type": "integer",
              "example": 100
            },
            "required": false
          }
        ],
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/admin/script-risk/decisions/{id}": {
      "get": {
        "operationId": "admin_script_risk_decision_detail",
        "tags": [
          "admin"
        ],
        "summary": "Get one script-risk decision",
        "description": "Full audit record for one script-risk gate decision.",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "schema": {
              "type": "integer"
            },
            "required": true
          }
        ],
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/consent/grants": {
      "get": {
        "operationId": "consent_grants_list",
        "tags": [
          "admin"
        ],
        "summary": "List tool-consent grants",
        "description": "List authoring-time consent grants for Tier-3 / Tier-4 tools running from cron / workflow / agent sources. Each grant binds (scope, tool_name, args_constraints). Filter by `scope_kind` + `scope_id`, `user_id`, or include revoked rows via `include_revoked=true`.",
        "parameters": [
          {
            "in": "query",
            "name": "scope_kind",
            "schema": {
              "type": "string",
              "enum": [
                "workflow_definition",
                "cron",
                "agent"
              ]
            },
            "required": false
          },
          {
            "in": "query",
            "name": "scope_id",
            "schema": {
              "type": "string"
            },
            "required": false
          },
          {
            "in": "query",
            "name": "user_id",
            "schema": {
              "type": "string"
            },
            "required": false
          },
          {
            "in": "query",
            "name": "include_revoked",
            "schema": {
              "type": "boolean"
            },
            "required": false
          }
        ],
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      },
      "post": {
        "operationId": "consent_grants_create",
        "tags": [
          "admin"
        ],
        "summary": "Create a tool-consent grant",
        "description": "Record operator authorization for a Tier-3 / Tier-4 tool call scheduled to run from an autonomous source. At least one constraint inside `args_constraints` MUST be set (`args_sha256`, `to_address`, `chain`, or `max_per_call_usdc`); an unconstrained grant is rejected.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              },
              "example": {
                "scope_kind": "cron",
                "scope_id": "cron_7f2",
                "tool_name": "transfer_token",
                "args_constraints": {
                  "to_address": "0xABCD...",
                  "chain": "base",
                  "max_per_call_usdc": 50
                },
                "note": "weekly USDC sweep"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/consent/grants/{id}": {
      "get": {
        "operationId": "consent_grants_show",
        "tags": [
          "admin"
        ],
        "summary": "Get one tool-consent grant",
        "description": "Return the full record for one consent grant.",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "schema": {
              "type": "string"
            },
            "required": true
          }
        ],
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      },
      "delete": {
        "operationId": "consent_grants_revoke",
        "tags": [
          "admin"
        ],
        "summary": "Revoke a tool-consent grant",
        "description": "Mark a grant revoked. Subsequent autonomous calls to the tool under the same scope will block again until a fresh grant is issued.",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "schema": {
              "type": "string"
            },
            "required": true
          }
        ],
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/consent/grants/requirements": {
      "post": {
        "operationId": "consent_grants_requirements",
        "tags": [
          "admin"
        ],
        "summary": "Compute grant requirements for a draft definition",
        "description": "Dry-run scan of a draft cron / workflow / agent definition for Tier-3 / Tier-4 tool calls that need an authoring-time consent grant. The wizard calls this before save, surfaces one prompt per returned requirement, then POSTs the approved grants to `/v1/consent/grants`.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              },
              "example": "{ \"scope_kind\": \"cron\", \"definition\": { /* WorkflowDefinition draft */ } }"
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/workflows/batch/actions": {
      "post": {
        "operationId": "workflow_batch_actions",
        "tags": [
          "workflows"
        ],
        "summary": "Batch workflow actions",
        "description": "Apply the same action (activate / deactivate / pause / delete) to many workflows in one request.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              },
              "example": {
                "ids": [
                  "wf_1",
                  "wf_2"
                ],
                "action": "activate"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/workflows/import/n8n": {
      "post": {
        "operationId": "workflow_import_n8n",
        "tags": [
          "workflows"
        ],
        "summary": "Import a workflow from n8n",
        "description": "Convert an n8n workflow JSON into a local Minara workflow definition. Idempotent: the same import id collapses on conflict.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              },
              "example": "{ \"n8n\": { /* n8n export */ } }"
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/workflows/{id}": {
      "get": {
        "operationId": "workflow_detail",
        "tags": [
          "workflows"
        ],
        "summary": "Get workflow detail",
        "description": "Full workflow definition by `<backend>:<rawId>` (or legacy unprefixed id, defaulting to local).",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "schema": {
              "type": "string"
            },
            "required": true
          }
        ],
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      },
      "patch": {
        "operationId": "workflow_update",
        "tags": [
          "workflows"
        ],
        "summary": "Update a workflow",
        "description": "Edit a workflow's draft / metadata / steps.",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "schema": {
              "type": "string"
            },
            "required": true
          }
        ],
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      },
      "delete": {
        "operationId": "workflow_delete",
        "tags": [
          "workflows"
        ],
        "summary": "Delete a workflow",
        "description": "Soft-delete a workflow definition; running instances continue.",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "schema": {
              "type": "string"
            },
            "required": true
          }
        ],
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/workflows/{id}/run": {
      "post": {
        "operationId": "workflow_run",
        "tags": [
          "workflows"
        ],
        "summary": "Run a workflow",
        "description": "Kick off a one-shot execution of the workflow.",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "schema": {
              "type": "string"
            },
            "required": true
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              },
              "example": {
                "execution_mode": "production",
                "dry_run": false
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/workflows/{id}/pause": {
      "post": {
        "operationId": "workflow_pause",
        "tags": [
          "workflows"
        ],
        "summary": "Pause a workflow",
        "description": "Pause the running instance(s) of a workflow.",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "schema": {
              "type": "string"
            },
            "required": true
          }
        ],
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/workflows/{id}/activate": {
      "post": {
        "operationId": "workflow_activate",
        "tags": [
          "workflows"
        ],
        "summary": "Activate a workflow",
        "description": "Set the workflow's `active` flag to true (eligible for triggers).",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "schema": {
              "type": "string"
            },
            "required": true
          }
        ],
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/workflows/{id}/deactivate": {
      "post": {
        "operationId": "workflow_deactivate",
        "tags": [
          "workflows"
        ],
        "summary": "Deactivate a workflow",
        "description": "Set the workflow's `active` flag to false (no new trigger fires).",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "schema": {
              "type": "string"
            },
            "required": true
          }
        ],
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/workflows/{id}/validate": {
      "post": {
        "operationId": "workflow_validate",
        "tags": [
          "workflows"
        ],
        "summary": "Validate a workflow draft",
        "description": "Static + simulated validation of a workflow draft before publish.",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "schema": {
              "type": "string"
            },
            "required": true
          }
        ],
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/workflows/{id}/publish": {
      "post": {
        "operationId": "workflow_publish",
        "tags": [
          "workflows"
        ],
        "summary": "Publish a workflow draft",
        "description": "Promote the draft revision to the active definition.",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "schema": {
              "type": "string"
            },
            "required": true
          }
        ],
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/workflows/{id}/test": {
      "post": {
        "operationId": "workflow_test",
        "tags": [
          "workflows"
        ],
        "summary": "Test a workflow",
        "description": "Dry-run a workflow with operator-supplied input for previewing.",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "schema": {
              "type": "string"
            },
            "required": true
          }
        ],
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/workflows/from-template": {
      "post": {
        "operationId": "workflow_from_template",
        "tags": [
          "workflows"
        ],
        "summary": "Create a workflow from a template",
        "description": "Render one of the curated landing-page templates into a real WorkflowDefinition via operator-supplied params. The gateway picks the matching template, runs the param-substitution pass, and saves the resulting draft like a hand-authored create.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              },
              "example": {
                "template_id": "crypto-price-alert",
                "params": {
                  "token": "BTC",
                  "threshold_pct": -5
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/workflows/generate": {
      "post": {
        "operationId": "workflow_generate",
        "tags": [
          "workflows"
        ],
        "summary": "Generate a workflow from a prompt",
        "description": "LLM authoring: turns a plain-language description into a validated WorkflowDefinition. Runs the workflow-authoring subagent with up to 3 attempts; each retry receives a structured error locator describing why the previous attempt failed validation. Streams events in attempt-bounded groups; the canvas updates on the terminal `workflow.commit` only. On validation failure after MAX_ATTEMPTS, emits `cluster.rollback{reason:'validation_failed'}` and skips the row creation.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              },
              "example": {
                "prompt": "Alert me when BTC drops 5% in 24h via Telegram."
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "text/event-stream. Events fire in attempt-bounded groups:\n  cluster.attempt_start { attempt, total_budget }\n  cluster.node_code_delta { step_name, delta }\n  cluster.node_add { step_name, kind, definition }\n  workflow.patch  (preview only — does NOT mutate the canvas)\n  cluster.attempt_discard { attempt, errors } OR cluster.validate { ok }\n  workflow.commit { definition, attempt }  (only on the validated attempt)\n  cluster.rollback { reason: 'messaging_gate' | 'persist_failed' | 'validation_failed' | 'stream_error' }\n  assistant.message { text }  (delta-streamed)\n  workflow.created { workflow, validation, ignored_fields, source }\n  error { message, code }",
            "content": {
              "text/event-stream": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/workflows/{id}/refine": {
      "post": {
        "operationId": "workflow_refine",
        "tags": [
          "workflows"
        ],
        "summary": "Refine a workflow via chat",
        "description": "Open an SSE stream that streams cluster events as the LLM edits the workflow definition in response to the user's plain-language message. The workflow-authoring subagent retries up to 3 times when validation fails; the chat panel renders each attempt as a collapsible section and the canvas only updates on the validated attempt's terminal `workflow.commit`. Persists user + assistant chat turns; the assistant row stays `status='in_progress'` while live and finalizes on completion. Returns 409 if another refine session is already active for this workflow — clients should attach to the existing session via `GET /v1/workflows/:id/refine/stream` instead of opening a parallel run. On validation failure after MAX_ATTEMPTS the definition is NOT persisted; the existing definition stays as-is.",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "schema": {
              "type": "string"
            },
            "required": true
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              },
              "example": {
                "message": "Send me an alert when BTC drops 5%."
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "text/event-stream. Events fire in attempt-bounded groups:\n  cluster.attempt_start { attempt, total_budget }\n  cluster.node_code_delta { step_name, delta }\n  cluster.node_add { step_name, kind, definition }\n  workflow.patch  (preview only — does NOT mutate the canvas)\n  cluster.attempt_discard { attempt, errors } OR cluster.validate { ok }\n  workflow.commit { definition, attempt }  (only on the validated attempt)\n  cluster.rollback { reason: 'messaging_gate' | 'persist_failed' | 'validation_failed' | 'stream_error' }\n  assistant.message { text }  (delta-streamed)\n  error { message, code }",
            "content": {
              "text/event-stream": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/workflows/{id}/refine/stream": {
      "get": {
        "operationId": "workflow_refine_stream",
        "tags": [
          "workflows"
        ],
        "summary": "Attach to an in-flight refine session",
        "description": "SSE endpoint that replays the active refine session's event buffer in order, then tails live events until the session finishes. Used by the web UI to resume the chat stream after a mid-stream refresh or navigation. Replays the SAME attempt-bounded event vocabulary the POST /refine endpoint streams. Discarded attempts from before the attach reach the client through the persisted chat history (chat_messages.metadata.events), not via this live attach. Returns 204 No Content when no session is active for this workflow.",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "schema": {
              "type": "string"
            },
            "required": true
          }
        ],
        "responses": {
          "200": {
            "description": "text/event-stream — same envelope as POST /refine, OR HTTP 204 when no active session.",
            "content": {
              "text/event-stream": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/workflows/{id}/executions": {
      "get": {
        "operationId": "workflow_executions_list",
        "tags": [
          "workflows"
        ],
        "summary": "List workflow executions",
        "description": "Recent execution instances for one workflow.",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "schema": {
              "type": "string"
            },
            "required": true
          }
        ],
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/workflows/{id}/export/n8n": {
      "get": {
        "operationId": "workflow_export_n8n",
        "tags": [
          "workflows"
        ],
        "summary": "Export workflow to n8n",
        "description": "Convert a Minara workflow back to the n8n JSON format.",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "schema": {
              "type": "string"
            },
            "required": true
          }
        ],
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/workflows/{id}/executions/{instanceId}": {
      "get": {
        "operationId": "workflow_execution_get",
        "tags": [
          "workflows"
        ],
        "summary": "Get one execution",
        "description": "Full state for one workflow execution instance.",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "schema": {
              "type": "string"
            },
            "required": true
          },
          {
            "in": "path",
            "name": "instanceId",
            "schema": {
              "type": "string"
            },
            "required": true
          }
        ],
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      },
      "delete": {
        "operationId": "workflow_execution_delete",
        "tags": [
          "workflows"
        ],
        "summary": "Delete an execution record",
        "description": "Remove the persisted record of one execution instance.",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "schema": {
              "type": "string"
            },
            "required": true
          },
          {
            "in": "path",
            "name": "instanceId",
            "schema": {
              "type": "string"
            },
            "required": true
          }
        ],
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/workflows/{id}/instances/{instanceId}/resume": {
      "post": {
        "operationId": "workflow_instance_resume",
        "tags": [
          "workflows"
        ],
        "summary": "Resume a paused instance",
        "description": "Resume an instance that's currently waiting on a confirm step.",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "schema": {
              "type": "string"
            },
            "required": true
          },
          {
            "in": "path",
            "name": "instanceId",
            "schema": {
              "type": "string"
            },
            "required": true
          }
        ],
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/workflows/{id}/instances/{instanceId}/cancel": {
      "post": {
        "operationId": "workflow_instance_cancel",
        "tags": [
          "workflows"
        ],
        "summary": "Cancel a running instance",
        "description": "Cancel a running execution. The cancellation reason is logged.",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "schema": {
              "type": "string"
            },
            "required": true
          },
          {
            "in": "path",
            "name": "instanceId",
            "schema": {
              "type": "string"
            },
            "required": true
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              },
              "example": {
                "reason": "operator cancelled"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/sandbox/files/{name}": {
      "get": {
        "operationId": "sandbox_files_fetch",
        "tags": [
          "files"
        ],
        "summary": "Fetch a sandbox file",
        "description": "Read a file the agent wrote into its per-session sandbox. Cannot escape the sandbox root.",
        "parameters": [
          {
            "in": "path",
            "name": "name",
            "schema": {
              "type": "string"
            },
            "required": true
          }
        ],
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/interactions/{id}/answer": {
      "post": {
        "operationId": "interactions_answer",
        "tags": [
          "state"
        ],
        "summary": "Answer an interaction prompt",
        "description": "Submit the user's answer to a paused agent prompt (the agent loop stops on `interaction_required` events; the web UI / CLI calls this to deliver the response).",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "schema": {
              "type": "string"
            },
            "required": true
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              },
              "example": {
                "value": "yes"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/studio/p/{pageId}": {
      "get": {
        "operationId": "studio_page_host",
        "tags": [
          "gateway"
        ],
        "summary": "Hosted Data Studio dashboard",
        "description": "Serve a generated Data Studio dashboard as a standalone HTML page on its own link. Public (no auth) so the page can be shared; rendered inside a sandboxed cross-origin iframe with a strict CSP. Returns `text/html`, not JSON.",
        "parameters": [
          {
            "in": "path",
            "name": "pageId",
            "schema": {
              "type": "string"
            },
            "required": true
          }
        ],
        "responses": {
          "200": {
            "description": "Success"
          }
        },
        "security": []
      }
    },
    "/v1/chat/sessions/{id}/goal": {
      "get": {
        "operationId": "session_goal_get",
        "tags": [
          "chat"
        ],
        "summary": "Get the session's standing goal",
        "description": "Return the session's standing goal, or `null` when none is set. The agent pursues a standing goal turn after turn: after each turn a judge decides whether the goal is met, and if not the client sends the next turn automatically (the chat stream carries the decision).",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "schema": {
              "type": "string"
            },
            "required": true
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "goal": {
                    "goalText": "ship the report",
                    "status": "active",
                    "turnsUsed": 3,
                    "maxTurns": 20,
                    "subgoals": [],
                    "lastVerdict": "continue",
                    "lastReason": "draft not finished",
                    "pausedReason": null
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      },
      "post": {
        "operationId": "session_goal_set",
        "tags": [
          "chat"
        ],
        "summary": "Set the session's standing goal",
        "description": "Set a standing goal for the session. Rejected with 400 when a goal is already active or paused — clear it first. `maxTurns` is the turn budget for one run (defaults to the configured value).",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "schema": {
              "type": "string"
            },
            "required": true
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              },
              "example": {
                "goalText": "ship the report",
                "maxTurns": 20
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "ok": true,
                  "goal": {
                    "goalText": "ship the report",
                    "status": "active",
                    "turnsUsed": 0,
                    "maxTurns": 20,
                    "subgoals": [],
                    "lastVerdict": null,
                    "lastReason": null,
                    "pausedReason": null
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/chat/sessions/{id}/goal/pause": {
      "post": {
        "operationId": "session_goal_pause",
        "tags": [
          "chat"
        ],
        "summary": "Pause the standing goal",
        "description": "Pause the standing goal's continuation. Manual turns still work; the goal can be resumed later.",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "schema": {
              "type": "string"
            },
            "required": true
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "ok": true,
                  "goal": {
                    "status": "paused",
                    "pausedReason": "user"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/chat/sessions/{id}/goal/resume": {
      "post": {
        "operationId": "session_goal_resume",
        "tags": [
          "chat"
        ],
        "summary": "Resume the standing goal",
        "description": "Resume a paused goal. Resets the turn budget so the goal gets a fresh run of turns.",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "schema": {
              "type": "string"
            },
            "required": true
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "ok": true,
                  "goal": {
                    "status": "active",
                    "turnsUsed": 0
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/chat/sessions/{id}/goal/clear": {
      "post": {
        "operationId": "session_goal_clear",
        "tags": [
          "chat"
        ],
        "summary": "Clear the standing goal",
        "description": "Clear the standing goal. Terminal — the goal is gone.",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "schema": {
              "type": "string"
            },
            "required": true
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "ok": true,
                  "goal": null
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/chat/sessions/{id}/goal/subgoals": {
      "post": {
        "operationId": "session_goal_subgoal_add",
        "tags": [
          "chat"
        ],
        "summary": "Add an acceptance criterion",
        "description": "Append an acceptance criterion to the standing goal. The judge and the next-turn prompt both carry the criteria. Works on an active or paused goal.",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "schema": {
              "type": "string"
            },
            "required": true
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              },
              "example": {
                "criterion": "all tests pass"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "ok": true,
                  "goal": {
                    "subgoals": [
                      "all tests pass"
                    ]
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    },
    "/v1/chat/sessions/{id}/goal/subgoals/{n}": {
      "delete": {
        "operationId": "session_goal_subgoal_drop",
        "tags": [
          "chat"
        ],
        "summary": "Drop an acceptance criterion",
        "description": "Remove the acceptance criterion at position `n` (1-based). Rejected with 400 when `n` is out of range.",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "schema": {
              "type": "string"
            },
            "required": true
          },
          {
            "in": "path",
            "name": "n",
            "schema": {
              "type": "integer",
              "example": 1
            },
            "required": true
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "ok": true,
                  "goal": {
                    "subgoals": []
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer token"
          }
        }
      }
    }
  }
}
