{
  "openapi": "3.1.0",
  "info": {
    "title": "Solana Risk Radar API",
    "version": "1.0.0",
    "description": "Read-only Solana SPL/Token-2022 risk scanner. Every report carries an evidence hash. The API never signs or sends transactions.",
    "license": { "name": "UNLICENSED" }
  },
  "servers": [{ "url": "http://127.0.0.1:8097", "description": "Loopback (SSH tunnel in production)" }],
  "tags": [
    { "name": "scans" },
    { "name": "tokens" },
    { "name": "radar" },
    { "name": "watchlist" },
    { "name": "health" }
  ],
  "components": {
    "securitySchemes": {
      "adminToken": { "type": "http", "scheme": "bearer", "description": "ADMIN_TOKEN (min 24 chars)" }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "properties": { "error": { "type": "string" }, "detail": { "type": "string" } },
        "required": ["error"]
      },
      "ScanCreated": {
        "type": "object",
        "properties": {
          "scan_id": { "type": "string", "format": "uuid" },
          "status": { "$ref": "#/components/schemas/ScanStatus" },
          "reused": { "type": "boolean" }
        },
        "required": ["scan_id", "status", "reused"]
      },
      "ScanStatus": {
        "type": "string",
        "enum": ["PENDING", "RUNNING", "COMPLETE", "PARTIAL", "TOO_EARLY", "TOO_ESTABLISHED", "UNSUPPORTED", "FAILED", "STALE"]
      },
      "Verdict": { "type": "string", "enum": ["CLEAN", "OK", "RISKY", "DANGER", "SIN_SCORE"] },
      "Step": {
        "type": "object",
        "properties": {
          "name": { "type": "string" },
          "status": { "type": "string", "enum": ["PENDING", "RUNNING", "OK", "SKIPPED", "PARTIAL", "FAILED"] },
          "detail": { "type": ["string", "null"] }
        },
        "required": ["name", "status"]
      },
      "ScanState": {
        "type": "object",
        "properties": {
          "scan": {
            "type": "object",
            "properties": {
              "id": { "type": "string" },
              "mint": { "type": "string" },
              "status": { "$ref": "#/components/schemas/ScanStatus" },
              "ruleset": { "type": "string" },
              "raw_score": { "type": ["number", "null"] },
              "adjusted_score": { "type": ["number", "null"] },
              "verdict": { "type": ["string", "null"] },
              "created_at": { "type": "string", "format": "date-time" },
              "finished_at": { "type": ["string", "null"] },
              "error": { "type": ["string", "null"] }
            },
            "required": ["id", "mint", "status"]
          },
          "steps": { "type": "array", "items": { "$ref": "#/components/schemas/Step" } },
          "queue": {
            "type": "object",
            "properties": { "running": { "type": "integer" }, "pending": { "type": "integer" } }
          }
        },
        "required": ["scan", "steps"]
      },
      "Holder": {
        "type": "object",
        "properties": {
          "tokenAccount": { "type": "string" },
          "owner": { "type": ["string", "null"] },
          "amountRaw": { "type": "string", "description": "decimal string (BigInt)" },
          "amountUi": { "type": "number" },
          "pctSupply": { "type": "number" },
          "pctFloat": { "type": "number" },
          "rank": { "type": "integer" },
          "excluded": { "type": "boolean" },
          "excludeReason": { "type": ["string", "null"] }
        },
        "required": ["tokenAccount", "amountRaw", "rank", "excluded"]
      },
      "ScoreComponent": {
        "type": "object",
        "properties": {
          "code": { "type": "string" },
          "label": { "type": "string" },
          "observed": { "type": ["number", "null"] },
          "points": { "type": ["number", "null"] },
          "maxPoints": { "type": "number" },
          "detail": { "type": "string" }
        },
        "required": ["code", "maxPoints"]
      },
      "Simulation": {
        "type": "object",
        "properties": {
          "supported": { "type": "boolean" },
          "reason": { "type": "string" },
          "priceImpact": { "type": ["number", "null"] },
          "spotPrice": { "type": ["number", "null"] },
          "execPrice": { "type": ["number", "null"] },
          "newSpot": { "type": ["number", "null"] },
          "feeBps": { "type": ["number", "null"] },
          "poolAddress": { "type": ["string", "null"] },
          "formula": { "type": ["string", "null"] },
          "assumptions": { "type": "array", "items": { "type": "string" } }
        },
        "required": ["supported", "reason"]
      },
      "Report": {
        "type": "object",
        "properties": {
          "scan_id": { "type": "string" },
          "mint": { "type": "string" },
          "status": { "$ref": "#/components/schemas/ScanStatus" },
          "ruleset": { "type": "string" },
          "generated_at": { "type": "string", "format": "date-time" },
          "holders": { "type": "array", "items": { "$ref": "#/components/schemas/Holder" } },
          "simulation": { "$ref": "#/components/schemas/Simulation" },
          "score": {
            "type": "object",
            "properties": {
              "rawScore": { "type": ["number", "null"] },
              "adjustedScore": { "type": ["number", "null"] },
              "verdict": { "$ref": "#/components/schemas/Verdict" },
              "components": { "type": "array", "items": { "$ref": "#/components/schemas/ScoreComponent" } },
              "overrides": { "type": "array", "items": { "type": "object" } },
              "coverage": { "type": "object" }
            },
            "required": ["verdict", "components"]
          },
          "risks": { "type": "array", "items": { "type": "object" } },
          "evidence_hash": { "type": "string", "description": "SHA-256 of the report without this field" }
        },
        "required": ["scan_id", "mint", "status", "evidence_hash"]
      }
    }
  },
  "paths": {
    "/health/live": {
      "get": {
        "tags": ["health"],
        "summary": "Liveness",
        "responses": { "200": { "description": "Alive", "content": { "application/json": { "schema": { "type": "object", "properties": { "status": { "type": "string" }, "uptime_s": { "type": "integer" } } } } } } }
      }
    },
    "/health/ready": {
      "get": {
        "tags": ["health"],
        "summary": "Readiness (no secrets)",
        "responses": { "200": { "description": "Ready" }, "503": { "description": "Not ready", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } }
      }
    },
    "/admin/health": {
      "get": {
        "tags": ["health"],
        "summary": "Admin telemetry: RPC pool, queue, radar, counters",
        "security": [{ "adminToken": [] }],
        "responses": {
          "200": { "description": "Telemetry" },
          "401": { "description": "Unauthorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/api/v1/scans": {
      "post": {
        "tags": ["scans"],
        "summary": "Create an idempotent scan job",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "mint": { "type": "string", "description": "base58 SPL mint (32 bytes)" },
                  "refresh": { "type": "boolean", "default": false }
                },
                "required": ["mint"]
              }
            }
          }
        },
        "responses": {
          "200": { "description": "Reused existing scan", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ScanCreated" } } } },
          "202": { "description": "Queued", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ScanCreated" } } } },
          "403": { "description": "public_scans_disabled", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "422": { "description": "invalid_base58 / mint_required", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "429": { "description": "rate_limited / queue_full", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/api/v1/scans/{id}": {
      "get": {
        "tags": ["scans"],
        "summary": "Scan state and steps",
        "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } }],
        "responses": {
          "200": { "description": "State", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ScanState" } } } },
          "404": { "description": "scan_not_found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/api/v1/scans/{id}/report": {
      "get": {
        "tags": ["scans"],
        "summary": "Frozen report snapshot",
        "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } }],
        "responses": {
          "200": { "description": "Report", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Report" } } } },
          "409": { "description": "report_not_ready" }
        }
      }
    },
    "/api/v1/scans/{id}/events": {
      "get": {
        "tags": ["scans"],
        "summary": "Server-Sent Events progress stream",
        "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } }],
        "responses": { "200": { "description": "text/event-stream", "content": { "text/event-stream": { "schema": { "type": "string" } } } } }
      }
    },
    "/api/v1/scans/{id}/export.json": {
      "get": {
        "tags": ["scans"],
        "summary": "Download report JSON",
        "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } }],
        "responses": { "200": { "description": "Attachment" } }
      }
    },
    "/api/v1/scans/{id}/export.csv": {
      "get": {
        "tags": ["scans"],
        "summary": "Download report CSV (UTF-8 BOM)",
        "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } }],
        "responses": { "200": { "description": "Attachment", "content": { "text/csv": { "schema": { "type": "string" } } } } }
      }
    },
    "/api/v1/tokens/{mint}": {
      "get": {
        "tags": ["tokens"],
        "summary": "Token summary and price history",
        "parameters": [
          { "name": "mint", "in": "path", "required": true, "schema": { "type": "string" } },
          { "name": "scan_id", "in": "query", "schema": { "type": "string" } }
        ],
        "responses": { "200": { "description": "Summary" } }
      }
    },
    "/api/v1/tokens/{mint}/holders": {
      "get": {
        "tags": ["tokens"],
        "summary": "Paginated holders (cursor)",
        "parameters": [
          { "name": "mint", "in": "path", "required": true, "schema": { "type": "string" } },
          { "name": "scan_id", "in": "query", "schema": { "type": "string" } },
          { "name": "limit", "in": "query", "schema": { "type": "integer", "maximum": 100, "default": 50 } },
          { "name": "cursor", "in": "query", "schema": { "type": "string" } }
        ],
        "responses": { "200": { "description": "Holders + next_cursor" }, "404": { "description": "no_scan_for_mint" } }
      }
    },
    "/api/v1/tokens/{mint}/graph": {
      "get": {
        "tags": ["tokens"],
        "summary": "Wallet clusters and edges",
        "parameters": [
          { "name": "mint", "in": "path", "required": true, "schema": { "type": "string" } },
          { "name": "scan_id", "in": "query", "schema": { "type": "string" } }
        ],
        "responses": { "200": { "description": "Graph" } }
      }
    },
    "/api/v1/tokens/{mint}/first-buyers": {
      "get": {
        "tags": ["tokens"],
        "summary": "Reconstructed first entries",
        "parameters": [{ "name": "mint", "in": "path", "required": true, "schema": { "type": "string" } }],
        "responses": { "200": { "description": "First buyers" } }
      }
    },
    "/api/v1/tokens/{mint}/liquidity": {
      "get": {
        "tags": ["tokens"],
        "summary": "Pools, liquidity guard and simulation",
        "parameters": [{ "name": "mint", "in": "path", "required": true, "schema": { "type": "string" } }],
        "responses": { "200": { "description": "Liquidity and simulation" } }
      }
    },
    "/api/v1/simulations": {
      "post": {
        "tags": ["tokens"],
        "summary": "Pure simulation (no on-chain side effects)",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "scan_id": { "type": "string" },
                  "amount_raw": { "type": "string" },
                  "amount_ui": { "type": "number" }
                },
                "required": ["scan_id"]
              }
            }
          }
        },
        "responses": { "200": { "description": "supported + result or reason" } }
      }
    },
    "/api/v1/radar": {
      "get": {
        "tags": ["radar"],
        "summary": "Launch radar items + feed health",
        "parameters": [
          { "name": "status", "in": "query", "schema": { "type": "string", "enum": ["detected", "data_incomplete", "passed_filters", "held_by_risk", "no_liquidity", "unsupported"] } },
          { "name": "limit", "in": "query", "schema": { "type": "integer", "maximum": 100 } },
          { "name": "cursor", "in": "query", "schema": { "type": "string" } }
        ],
        "responses": { "200": { "description": "Radar page" } }
      }
    },
    "/api/v1/watchlist": {
      "get": { "tags": ["watchlist"], "summary": "List watchlist", "responses": { "200": { "description": "Items" } } },
      "post": {
        "tags": ["watchlist"],
        "summary": "Add item (admin)",
        "security": [{ "adminToken": [] }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "kind": { "type": "string", "enum": ["token", "wallet"] },
                  "address": { "type": "string" },
                  "label": { "type": "string" }
                },
                "required": ["kind", "address"]
              }
            }
          }
        },
        "responses": { "201": { "description": "Created" }, "401": { "description": "Unauthorized" }, "422": { "description": "invalid_watchlist_item" } }
      },
      "delete": {
        "tags": ["watchlist"],
        "summary": "Remove by address (admin)",
        "security": [{ "adminToken": [] }],
        "parameters": [{ "name": "address", "in": "query", "required": true, "schema": { "type": "string" } }],
        "responses": { "200": { "description": "Deleted" }, "401": { "description": "Unauthorized" } }
      }
    },
    "/api/v1/alerts": {
      "get": {
        "tags": ["watchlist"],
        "summary": "Recent deduplicated alerts",
        "responses": { "200": { "description": "Alerts" } }
      }
    },
    "/api/v1/stats": {
      "get": {
        "tags": ["health"],
        "summary": "Public counters",
        "responses": { "200": { "description": "Stats" } }
      }
    }
  }
}
