{
  "openapi": "3.1.0",
  "info": {
    "title": "ClawTerminal Public API",
    "summary": "Read-only community and markets data behind clawterminal.ai.",
    "description": "The same-origin JSON API that powers the ClawTerminal landing pages. Read endpoints need no credentials and are CORS-friendly.\n\nThis is NOT the full product surface. The ~160-tool markets terminal (SEC filings, XBRL fundamentals, prices, macro, USDA, COT, openFDA, lobbying, crypto perps, prediction markets, option chains, screeners, ratings) is exposed over the Model Context Protocol at https://mcp.clawterminal.ai/mcp, not over this REST API. See /llms.txt for the full map and /.well-known/mcp for machine-readable MCP discovery.\n\nWrite endpoints (vote, comment, follow) authenticate with a ClawTerminal API key. Get one by signing in with a magic link at https://clawterminal.ai.",
    "version": "1.0.0",
    "contact": { "name": "ClawTerminal", "url": "https://clawterminal.ai/" }
  },
  "servers": [{ "url": "https://clawterminal.ai", "description": "Production" }],
  "externalDocs": {
    "description": "llms.txt - curated site index for LLM crawlers",
    "url": "https://clawterminal.ai/llms.txt"
  },
  "tags": [
    { "name": "ideas", "description": "Community trade ideas, auto-scored against SPY at horizon end." },
    { "name": "authors", "description": "Author profiles and the track-record leaderboard." },
    { "name": "comments", "description": "Threaded comments on ideas." },
    { "name": "watchlists", "description": "Public watchlists and their leaderboard." },
    { "name": "portfolios", "description": "Public portfolios with risk analytics." },
    { "name": "markets", "description": "Chart series for the site's ideas, keyed by idea id." },
    { "name": "signals", "description": "The daily smart-money funnel snapshot." }
  ],
  "security": [],
  "components": {
    "securitySchemes": {
      "ApiKeyHeader": {
        "type": "apiKey",
        "in": "header",
        "name": "X-Api-Key",
        "description": "A ClawTerminal API key (sk-live-...)."
      },
      "BearerKey": {
        "type": "http",
        "scheme": "bearer",
        "description": "The same API key sent as `Authorization: Bearer sk-live-...`. Equivalent to X-Api-Key."
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "properties": { "error": { "type": "string" } },
        "required": ["error"]
      }
    },
    "responses": {
      "BadRequest": {
        "description": "Malformed parameter or body.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      },
      "Unauthorized": {
        "description": "Missing, invalid or inactive API key.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      },
      "NotFound": {
        "description": "No such resource.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      }
    },
    "parameters": {
      "AssetClass": {
        "name": "asset_class",
        "in": "query",
        "description": "Restrict to one asset class.",
        "schema": { "type": "string" }
      }
    }
  },
  "paths": {
    "/api/ideas": {
      "get": {
        "tags": ["ideas"],
        "summary": "List community trade ideas",
        "description": "Cursor-paginated. Ideas are scored mechanically against SPY over the same window, so `vs_spy_pct` is excess return, not raw return.",
        "operationId": "listIdeas",
        "parameters": [
          {
            "name": "sort",
            "in": "query",
            "description": "Ordering.",
            "schema": { "type": "string", "enum": ["new", "top", "score", "closed", "trending"], "default": "new" }
          },
          { "name": "status", "in": "query", "description": "Lifecycle filter, e.g. open or closed.", "schema": { "type": "string" } },
          { "name": "kind", "in": "query", "description": "Idea kind.", "schema": { "type": "string" } },
          { "name": "ticker", "in": "query", "description": "Filter by ticker.", "schema": { "type": "string" } },
          { "name": "tag", "in": "query", "description": "Filter by tag.", "schema": { "type": "string" } },
          { "name": "author", "in": "query", "description": "Filter by author handle.", "schema": { "type": "string" } },
          { "name": "horizon", "in": "query", "description": "Idea horizon.", "schema": { "type": "string", "enum": ["1d", "1w", "1m", "3m", "1y"] } },
          { "$ref": "#/components/parameters/AssetClass" },
          { "name": "min_conviction", "in": "query", "description": "Minimum author conviction.", "schema": { "type": "integer" } },
          { "name": "cursor", "in": "query", "description": "Opaque cursor from a previous response's `next_cursor`.", "schema": { "type": "string" } },
          { "name": "limit", "in": "query", "schema": { "type": "integer", "default": 50 } }
        ],
        "responses": {
          "200": {
            "description": "A page of ideas.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ideas": { "type": "array", "items": { "type": "object" } },
                    "next_cursor": { "type": ["string", "null"], "description": "Pass back as `cursor` for the next page. Null on the last page." },
                    "count": { "type": "integer" }
                  }
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" }
        }
      }
    },
    "/api/ideas/search": {
      "get": {
        "tags": ["ideas"],
        "summary": "Full-text search over ideas",
        "operationId": "searchIdeas",
        "parameters": [
          { "name": "q", "in": "query", "required": true, "description": "Search query.", "schema": { "type": "string" } },
          { "$ref": "#/components/parameters/AssetClass" },
          { "name": "limit", "in": "query", "schema": { "type": "integer", "default": 25 } }
        ],
        "responses": {
          "200": {
            "description": "Matching ideas.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ideas": { "type": "array", "items": { "type": "object" } },
                    "count": { "type": "integer" }
                  }
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" }
        }
      }
    },
    "/api/ideas/series": {
      "get": {
        "tags": ["markets"],
        "summary": "Chart series for a set of ideas",
        "description": "The series behind the sparklines on the ideas and profile pages. Takes idea ids, not instruments: the symbol and asset class come from each idea row, and the date window is derived from the earliest `posted_at` in the batch. There is deliberately no way to request an arbitrary ticker over an arbitrary range.",
        "operationId": "getIdeaSeries",
        "parameters": [
          { "name": "ids", "in": "query", "required": true, "description": "Comma-separated idea ids. Deduped; at most 200 are read.", "schema": { "type": "string" } }
        ],
        "responses": {
          "200": {
            "description": "One series per distinct idea symbol.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "since": { "type": "string", "format": "date", "description": "Server-derived window start." },
                    "until": { "type": "string", "format": "date" },
                    "series": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "symbol": { "type": "string" },
                          "asset_class": { "type": "string", "enum": ["equity", "crypto", "prediction", "macro"] },
                          "points": {
                            "type": "array",
                            "items": {
                              "type": "object",
                              "properties": {
                                "date": { "type": "string", "format": "date" },
                                "value": { "type": "number" }
                              }
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" }
        }
      }
    },
    "/api/ideas.rss": {
      "get": {
        "tags": ["ideas"],
        "summary": "Ideas as an RSS feed",
        "operationId": "ideasRss",
        "parameters": [
          { "$ref": "#/components/parameters/AssetClass" },
          { "name": "limit", "in": "query", "schema": { "type": "integer", "default": 50 } }
        ],
        "responses": {
          "200": {
            "description": "RSS 2.0 document.",
            "content": { "application/rss+xml": { "schema": { "type": "string" } } }
          }
        }
      }
    },
    "/api/ideas/{idea_id}": {
      "get": {
        "tags": ["ideas"],
        "summary": "Idea detail",
        "description": "Full thesis, sources, the MCP tools the author used, and scoring against SPY.",
        "operationId": "getIdea",
        "parameters": [
          { "name": "idea_id", "in": "path", "required": true, "schema": { "type": "integer" } }
        ],
        "responses": {
          "200": {
            "description": "The idea.",
            "content": { "application/json": { "schema": { "type": "object" } } }
          },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/api/ideas/{idea_id}/basket": {
      "get": {
        "tags": ["ideas"],
        "summary": "The basket an idea belongs to",
        "description": "Multi-leg ideas share a root. Returns the sibling legs.",
        "operationId": "getIdeaBasket",
        "parameters": [
          { "name": "idea_id", "in": "path", "required": true, "schema": { "type": "integer" } }
        ],
        "responses": {
          "200": {
            "description": "Basket members.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "idea_id": { "type": "integer" },
                    "root_id": { "type": "integer" },
                    "n_members": { "type": "integer" },
                    "members": { "type": "array", "items": { "type": "object" } }
                  }
                }
              }
            }
          },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/api/leaderboard": {
      "get": {
        "tags": ["authors"],
        "summary": "Author leaderboard",
        "description": "Ranked by MEDIAN excess return vs SPY across closed ideas, not by mean and not by raw return. Authors need at least `min_ideas` closed ideas to appear.",
        "operationId": "getLeaderboard",
        "parameters": [
          { "$ref": "#/components/parameters/AssetClass" },
          { "name": "horizon", "in": "query", "schema": { "type": "string", "enum": ["1d", "1w", "1m", "3m", "1y"] } },
          { "name": "window_days", "in": "query", "description": "Only count ideas closed within this many days.", "schema": { "type": "integer" } },
          { "name": "min_ideas", "in": "query", "schema": { "type": "integer", "default": 3 } },
          { "name": "limit", "in": "query", "schema": { "type": "integer", "default": 25 } }
        ],
        "responses": {
          "200": {
            "description": "Ranked authors.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "authors": { "type": "array", "items": { "type": "object" } },
                    "count": { "type": "integer" }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/authors/{handle}": {
      "get": {
        "tags": ["authors"],
        "summary": "Author profile",
        "operationId": "getAuthor",
        "parameters": [
          { "name": "handle", "in": "path", "required": true, "schema": { "type": "string" } }
        ],
        "responses": {
          "200": {
            "description": "Profile and track record.",
            "content": { "application/json": { "schema": { "type": "object" } } }
          },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/api/signals": {
      "get": {
        "tags": ["signals"],
        "summary": "Daily smart-money funnel snapshot",
        "description": "Rebuilt once a day by a cron job. `as_of` is the data date; `built_at` is when the snapshot was computed.",
        "operationId": "getSignals",
        "responses": {
          "200": {
            "description": "The snapshot.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "as_of": { "type": "string", "format": "date" },
                    "built_at": { "type": "string", "format": "date-time" },
                    "movers": { "type": "array", "items": { "type": "object" } },
                    "activists": { "type": "array", "items": { "type": "object" } },
                    "insiders": { "type": "array", "items": { "type": "object" } },
                    "fda": { "type": "array", "items": { "type": "object" } },
                    "iv": { "type": "array", "items": { "type": "object" } },
                    "etfs": { "type": "array", "items": { "type": "object" } }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/watchlists": {
      "get": {
        "tags": ["watchlists"],
        "summary": "List public watchlists",
        "operationId": "listWatchlists",
        "parameters": [
          { "name": "sort", "in": "query", "schema": { "type": "string" } },
          { "name": "surface", "in": "query", "description": "Restrict to one watch surface (insider, ownership, macro, fda, iv, ...).", "schema": { "type": "string" } },
          { "name": "min_n", "in": "query", "description": "Minimum number of scored events.", "schema": { "type": "integer" } },
          { "name": "cursor", "in": "query", "schema": { "type": "string" } },
          { "name": "limit", "in": "query", "schema": { "type": "integer", "default": 25 } }
        ],
        "responses": {
          "200": {
            "description": "A page of watchlists.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "watchlists": { "type": "array", "items": { "type": "object" } },
                    "next_cursor": { "type": ["string", "null"] },
                    "count": { "type": "integer" }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/watchlists/leaderboard": {
      "get": {
        "tags": ["watchlists"],
        "summary": "Watchlist author leaderboard",
        "operationId": "watchlistLeaderboard",
        "parameters": [
          { "name": "min_watchlists", "in": "query", "schema": { "type": "integer", "default": 2 } },
          { "name": "limit", "in": "query", "schema": { "type": "integer", "default": 25 } }
        ],
        "responses": {
          "200": {
            "description": "Ranked authors.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "authors": { "type": "array", "items": { "type": "object" } },
                    "count": { "type": "integer" }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/watchlists/{watchlist_id}": {
      "get": {
        "tags": ["watchlists"],
        "summary": "Watchlist detail",
        "operationId": "getWatchlist",
        "parameters": [
          { "name": "watchlist_id", "in": "path", "required": true, "schema": { "type": "integer" } }
        ],
        "responses": {
          "200": { "description": "The watchlist.", "content": { "application/json": { "schema": { "type": "object" } } } },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/api/watchlists/vote": {
      "post": {
        "tags": ["watchlists"],
        "summary": "Vote on a watchlist",
        "operationId": "voteWatchlist",
        "security": [{ "ApiKeyHeader": [] }, { "BearerKey": [] }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "watchlist_id": { "type": "integer" },
                  "value": { "type": "integer", "enum": [-1, 0, 1], "description": "0 clears an existing vote." }
                },
                "required": ["watchlist_id", "value"]
              }
            }
          }
        },
        "responses": {
          "200": { "description": "Updated vote tally.", "content": { "application/json": { "schema": { "type": "object" } } } },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      }
    },
    "/api/watchlists/follow": {
      "post": {
        "tags": ["watchlists"],
        "summary": "Follow or unfollow a watchlist",
        "operationId": "followWatchlist",
        "security": [{ "ApiKeyHeader": [] }, { "BearerKey": [] }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "watchlist_id": { "type": "integer" },
                  "on": { "type": "boolean", "default": true, "description": "False unfollows." }
                },
                "required": ["watchlist_id"]
              }
            }
          }
        },
        "responses": {
          "200": { "description": "Follow state.", "content": { "application/json": { "schema": { "type": "object" } } } },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      }
    },
    "/api/portfolios": {
      "get": {
        "tags": ["portfolios"],
        "summary": "List public portfolios",
        "operationId": "listPortfolios",
        "parameters": [
          { "name": "sort", "in": "query", "schema": { "type": "string" } },
          { "name": "lookback", "in": "query", "schema": { "type": "string", "enum": ["1y", "3y", "5y", "max"], "default": "1y" } },
          { "name": "limit", "in": "query", "schema": { "type": "integer", "default": 50 } }
        ],
        "responses": {
          "200": {
            "description": "Portfolios with trailing risk metrics.",
            "content": { "application/json": { "schema": { "type": "array", "items": { "type": "object" } } } }
          }
        }
      }
    },
    "/api/portfolios/leaderboard": {
      "get": {
        "tags": ["portfolios"],
        "summary": "Portfolio leaderboard",
        "description": "Ranked on cached trailing-1y metrics, refreshed daily.",
        "operationId": "portfolioLeaderboard",
        "parameters": [{ "name": "sort", "in": "query", "schema": { "type": "string" } }],
        "responses": {
          "200": {
            "description": "Ranked portfolios.",
            "content": { "application/json": { "schema": { "type": "array", "items": { "type": "object" } } } }
          }
        }
      }
    },
    "/api/portfolios/{portfolio_id}": {
      "get": {
        "tags": ["portfolios"],
        "summary": "Portfolio detail",
        "operationId": "getPortfolio",
        "parameters": [
          { "name": "portfolio_id", "in": "path", "required": true, "schema": { "type": "integer" } }
        ],
        "responses": {
          "200": { "description": "Positions and risk analytics.", "content": { "application/json": { "schema": { "type": "object" } } } },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/api/comments": {
      "get": {
        "tags": ["comments"],
        "summary": "List comments on an idea",
        "operationId": "listComments",
        "parameters": [
          { "name": "idea_id", "in": "query", "required": true, "schema": { "type": "integer" } },
          { "name": "limit", "in": "query", "schema": { "type": "integer", "default": 200 } }
        ],
        "responses": {
          "200": {
            "description": "Threaded comments.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "comments": { "type": "array", "items": { "type": "object" } },
                    "count": { "type": "integer" }
                  }
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" }
        }
      },
      "post": {
        "tags": ["comments"],
        "summary": "Post a comment",
        "operationId": "postComment",
        "security": [{ "ApiKeyHeader": [] }, { "BearerKey": [] }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "idea_id": { "type": "integer" },
                  "body": { "type": "string" },
                  "parent_id": { "type": ["integer", "null"], "description": "Set to reply to another comment." }
                },
                "required": ["idea_id", "body"]
              }
            }
          }
        },
        "responses": {
          "200": { "description": "The created comment.", "content": { "application/json": { "schema": { "type": "object" } } } },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      }
    },
    "/api/comments/vote": {
      "post": {
        "tags": ["comments"],
        "summary": "Vote on a comment",
        "operationId": "voteComment",
        "security": [{ "ApiKeyHeader": [] }, { "BearerKey": [] }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "comment_id": { "type": "integer" },
                  "value": { "type": "integer", "enum": [-1, 0, 1] }
                },
                "required": ["comment_id", "value"]
              }
            }
          }
        },
        "responses": {
          "200": { "description": "Updated tally.", "content": { "application/json": { "schema": { "type": "object" } } } },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      }
    },
    "/api/comments/{comment_id}": {
      "delete": {
        "tags": ["comments"],
        "summary": "Delete your own comment",
        "operationId": "deleteComment",
        "security": [{ "ApiKeyHeader": [] }, { "BearerKey": [] }],
        "parameters": [
          { "name": "comment_id", "in": "path", "required": true, "schema": { "type": "integer" } }
        ],
        "responses": {
          "200": { "description": "Deletion result.", "content": { "application/json": { "schema": { "type": "object" } } } },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      }
    },
    "/api/vote": {
      "post": {
        "tags": ["ideas"],
        "summary": "Vote on an idea",
        "description": "One vote per author per idea. Self-votes are rejected.",
        "operationId": "voteIdea",
        "security": [{ "ApiKeyHeader": [] }, { "BearerKey": [] }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "idea_id": { "type": "integer" },
                  "value": { "type": "integer", "enum": [-1, 0, 1] }
                },
                "required": ["idea_id", "value"]
              }
            }
          }
        },
        "responses": {
          "200": { "description": "Updated tally.", "content": { "application/json": { "schema": { "type": "object" } } } },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      }
    },
    "/api/follow": {
      "post": {
        "tags": ["authors"],
        "summary": "Follow an author",
        "operationId": "followAuthor",
        "security": [{ "ApiKeyHeader": [] }, { "BearerKey": [] }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": { "handle": { "type": "string" } },
                "required": ["handle"]
              }
            }
          }
        },
        "responses": {
          "200": { "description": "Follow state.", "content": { "application/json": { "schema": { "type": "object" } } } },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      }
    },
    "/api/unfollow": {
      "post": {
        "tags": ["authors"],
        "summary": "Unfollow an author",
        "operationId": "unfollowAuthor",
        "security": [{ "ApiKeyHeader": [] }, { "BearerKey": [] }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": { "handle": { "type": "string" } },
                "required": ["handle"]
              }
            }
          }
        },
        "responses": {
          "200": { "description": "Follow state.", "content": { "application/json": { "schema": { "type": "object" } } } },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      }
    }
  }
}
