{
  "openapi": "3.1.0",
  "info": {
    "title": "RivalSnapshot Market Data API",
    "version": "1.0.0",
    "summary": "Local service-market intelligence for AI assistants: market size, the credible top-3 Map Pack, competitor rosters and AI-visibility reads.",
    "description": "Read-only access to RivalSnapshot's continuously refreshed market snapshots (Google Maps census, review-distribution gating, AI-answer polling). US markets only. The same five operations are exposed as MCP tools at POST /api/mcp (Streamable HTTP, JSON-RPC 2.0) and as REST on GET /api/market. Read operations are free and work without a key at 60 requests/min per caller; a valid `X-API-Key` lifts the limit to 600/min and is required for `order_report`. Ordering never charges by itself: the tool returns a checkout URL the user must open and complete on rivalsnapshot.com.",
    "termsOfService": "https://rivalsnapshot.com/terms",
    "contact": { "name": "RivalSnapshot support", "email": "support@rivalsnapshot.com", "url": "https://rivalsnapshot.com" },
    "x-privacy-policy": "https://rivalsnapshot.com/privacy",
    "x-security-contact": "security@rivalsnapshot.com"
  },
  "externalDocs": { "description": "Connector & API documentation", "url": "https://rivalsnapshot.com/docs/connector" },
  "servers": [{ "url": "https://rivalsnapshot.com" }],
  "tags": [
    { "name": "market", "description": "Free read operations (scope market:read)." },
    { "name": "mcp", "description": "Remote MCP server (Streamable HTTP)." }
  ],
  "security": [{}, { "ApiKeyAuth": [] }],
  "paths": {
    "/api/market": {
      "get": {
        "operationId": "getMarket",
        "tags": ["market"],
        "summary": "Market picture, competitor roster, or discovery list",
        "description": "Three variants on one route:\n\n- `?trade=&town=&state=` — the free market picture (MCP `get_market`). The same body also answers `get_competitors` (its `data.map_pack` / `data.top_competitors`) and `check_ai_visibility` (its `data.ai_visibility`).\n- `?list=1[&trade=&state=]` — mapped markets + freshness (MCP `list_markets`).\n- `?...&tier=full` — the full tier, metered via x402 (HTTP 402 with payment terms; settlement may be disabled).",
        "parameters": [
          { "name": "trade", "in": "query", "schema": { "type": "string" }, "description": "Service trade, e.g. `tree service`. Required unless `list=1`.", "example": "tree service" },
          { "name": "town", "in": "query", "schema": { "type": "string" }, "description": "Town or city. Required unless `list=1`.", "example": "Toms River" },
          { "name": "state", "in": "query", "schema": { "type": "string", "pattern": "^[A-Za-z]{2}$", "default": "NJ" }, "description": "Two-letter US state code.", "example": "NJ" },
          { "name": "tier", "in": "query", "schema": { "type": "string", "enum": ["free", "full"], "default": "free" }, "description": "`full` is x402-metered and answers 402 without a valid `X-PAYMENT` header." },
          { "name": "list", "in": "query", "schema": { "type": "string", "enum": ["1"] }, "description": "Set to `1` for the discovery list of mapped markets (optionally filtered by `trade` / `state`)." }
        ],
        "responses": {
          "200": {
            "description": "Market picture (free tier) or discovery list.",
            "content": {
              "application/json": {
                "schema": { "oneOf": [{ "$ref": "#/components/schemas/MarketFree" }, { "$ref": "#/components/schemas/MarketList" }] }
              }
            }
          },
          "400": { "description": "trade and town are required.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiError" } } } },
          "402": { "description": "Full tier: x402 payment requirements.", "content": { "application/json": { "schema": { "type": "object", "additionalProperties": true } } } },
          "404": { "description": "Market not mapped yet; `message` says how to request a free Market Map.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiError" } } } },
          "429": { "description": "Free-tier rate limit exceeded.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiError" } } } },
          "502": { "description": "Upstream data store unavailable." }
        }
      }
    },
    "/api/mcp": {
      "get": {
        "operationId": "describeMcp",
        "tags": ["mcp"],
        "summary": "Describe the MCP server",
        "responses": { "200": { "description": "Server name, protocol versions, auth, rate limits and tool list.", "content": { "application/json": { "schema": { "type": "object", "additionalProperties": true } } } } }
      },
      "post": {
        "operationId": "mcp",
        "tags": ["mcp"],
        "summary": "Remote MCP endpoint (JSON-RPC 2.0 over Streamable HTTP)",
        "description": "Stateless JSON-RPC 2.0. Methods: `initialize`, `notifications/initialized` (202, no body), `ping`, `tools/list`, `tools/call`. Single messages or batches. Responses are `application/json`; no SSE stream is opened.\n\nTools (also see `GET /api/mcp`):\n\n| tool | type | scope | inputs |\n|---|---|---|---|\n| `get_market` | read | market:read | trade, town, state? |\n| `list_markets` | read | market:read | trade?, state?, limit? |\n| `get_competitors` | read | market:read | trade, town, state? |\n| `check_ai_visibility` | read | market:read | business_name, trade, town, state? |\n| `order_report` | write, sensitive | report:order | trade, town, state, email?, plan? — requires `X-API-Key`; returns `checkout_url` with `requires_user_confirmation: true` |\n\nTool results carry `structuredContent` (the JSON object) plus a `text` content block with the same JSON. A not-found market answers inside the tool result (`ok:false, error:\"market_not_mapped\"` / `\"not_polled\"`), not as an HTTP error.",
        "security": [{}, { "ApiKeyAuth": [] }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "oneOf": [{ "$ref": "#/components/schemas/JsonRpcRequest" }, { "type": "array", "items": { "$ref": "#/components/schemas/JsonRpcRequest" } }] },
              "examples": {
                "initialize": { "value": { "jsonrpc": "2.0", "id": 1, "method": "initialize", "params": { "protocolVersion": "2025-06-18", "capabilities": {}, "clientInfo": { "name": "muse", "version": "1.0" } } } },
                "toolsList": { "value": { "jsonrpc": "2.0", "id": 2, "method": "tools/list" } },
                "getMarket": { "value": { "jsonrpc": "2.0", "id": 3, "method": "tools/call", "params": { "name": "get_market", "arguments": { "trade": "tree service", "town": "Toms River", "state": "NJ" } } } },
                "checkAiVisibility": { "value": { "jsonrpc": "2.0", "id": 4, "method": "tools/call", "params": { "name": "check_ai_visibility", "arguments": { "business_name": "Anthony's Tree Service", "trade": "tree service", "town": "Toms River", "state": "NJ" } } } },
                "orderReport": { "value": { "jsonrpc": "2.0", "id": 5, "method": "tools/call", "params": { "name": "order_report", "arguments": { "trade": "tree service", "town": "Toms River", "state": "NJ", "email": "owner@example.com" } } } }
              }
            }
          }
        },
        "responses": {
          "200": { "description": "JSON-RPC response (result or error object; array for a batch).", "content": { "application/json": { "schema": { "oneOf": [{ "$ref": "#/components/schemas/JsonRpcResponse" }, { "type": "array", "items": { "$ref": "#/components/schemas/JsonRpcResponse" } }] } } } },
          "202": { "description": "Notification accepted (no body)." },
          "400": { "description": "Parse error or invalid JSON-RPC request.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/JsonRpcResponse" } } } },
          "413": { "description": "Body larger than 64 KiB." },
          "429": { "description": "Rate limited (JSON-RPC error code -32000).", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/JsonRpcResponse" } } } }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "ApiKeyAuth": {
        "type": "apiKey",
        "in": "header",
        "name": "X-API-Key",
        "description": "Optional for read operations (lifts the free rate limit from 60 to 600 requests/min). Required for `order_report`. Unauthorized calls answer JSON-RPC error -32001."
      }
    },
    "schemas": {
      "ApiError": {
        "type": "object",
        "required": ["ok", "error"],
        "properties": { "ok": { "const": false }, "error": { "type": "string" }, "message": { "type": "string" }, "slug": { "type": "string" } }
      },
      "Competitor": {
        "type": "object",
        "properties": { "name": { "type": "string" }, "rating": { "type": ["number", "null"] }, "reviews": { "type": "integer" }, "website": { "type": ["string", "null"] } }
      },
      "AiVisibility": {
        "type": ["object", "null"],
        "description": "Null when the market has not been polled yet.",
        "properties": {
          "headline": { "type": "string", "description": "One honest sentence on who AI names in this market." },
          "overview_present": { "type": "boolean", "description": "Whether Google showed an AI Overview for the market keyword." },
          "named": { "type": "array", "items": { "type": "string" }, "description": "Businesses or domains AI currently names." }
        }
      },
      "MarketMeta": {
        "type": "object",
        "properties": {
          "slug": { "type": "string" }, "trade": { "type": "string" }, "town": { "type": "string" }, "state": { "type": "string" },
          "report_count": { "type": "integer" }, "last_refreshed_at": { "type": "string", "format": "date-time" }, "freshness_days": { "type": ["integer", "null"] }
        }
      },
      "MarketFree": {
        "allOf": [
          { "$ref": "#/components/schemas/MarketMeta" },
          {
            "type": "object",
            "required": ["ok", "tier", "data"],
            "properties": {
              "ok": { "const": true },
              "tier": { "const": "free" },
              "data": {
                "type": ["object", "null"],
                "properties": {
                  "market": { "type": "object", "properties": { "total_competitors": { "type": "integer" }, "avg_rating": { "type": ["number", "null"] }, "avg_review_count": { "type": ["number", "null"] } } },
                  "map_pack": { "type": "array", "maxItems": 3, "items": { "$ref": "#/components/schemas/Competitor" } },
                  "top_competitors": { "type": "array", "items": { "$ref": "#/components/schemas/Competitor" } },
                  "ai_visibility": { "$ref": "#/components/schemas/AiVisibility" }
                }
              }
            }
          }
        ]
      },
      "MarketList": {
        "type": "object",
        "required": ["ok", "count", "markets"],
        "properties": { "ok": { "const": true }, "count": { "type": "integer" }, "markets": { "type": "array", "maxItems": 200, "items": { "$ref": "#/components/schemas/MarketMeta" } } }
      },
      "JsonRpcRequest": {
        "type": "object",
        "required": ["jsonrpc", "method"],
        "properties": {
          "jsonrpc": { "const": "2.0" },
          "id": { "type": ["string", "number", "null"], "description": "Omit for notifications." },
          "method": { "type": "string", "enum": ["initialize", "notifications/initialized", "ping", "tools/list", "tools/call"] },
          "params": { "type": "object", "additionalProperties": true }
        }
      },
      "JsonRpcResponse": {
        "type": "object",
        "required": ["jsonrpc", "id"],
        "properties": {
          "jsonrpc": { "const": "2.0" },
          "id": { "type": ["string", "number", "null"] },
          "result": { "type": "object", "additionalProperties": true },
          "error": { "type": "object", "required": ["code", "message"], "properties": { "code": { "type": "integer" }, "message": { "type": "string" }, "data": {} } }
        }
      }
    }
  }
}
