{
  "openapi": "3.1.0",
  "info": {
    "title": "rye.dev public API",
    "version": "1.0.0",
    "description": "Public endpoints exposed for agent interaction: health, contact form, newsletter subscription, content reactions, the PII-free site pulse, the agent-traffic observatory, and the webmention receiver. Every path documented here is explicitly Allow'd in robots.txt; all other endpoints are private (admin/auth/cron) and disallowed there.",
    "contact": {
      "name": "Cameron Rye",
      "url": "https://rye.dev/about"
    },
    "license": {
      "name": "All rights reserved"
    }
  },
  "servers": [
    {
      "url": "https://rye.dev"
    }
  ],
  "paths": {
    "/api/health": {
      "get": {
        "summary": "Service health check",
        "description": "Returns 200 OK when the site and database are reachable.",
        "operationId": "getHealth",
        "responses": {
          "200": {
            "description": "Service is healthy",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HealthResponse"
                }
              }
            }
          },
          "503": {
            "description": "Service degraded or unavailable"
          }
        }
      }
    },
    "/api/contact": {
      "post": {
        "summary": "Submit contact form",
        "description": "Submit a contact message. Same-origin only — agents calling from a different origin will be rejected by middleware CSRF checks. Use the WebMCP `submit_contact` tool when operating in-browser.",
        "operationId": "submitContact",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ContactInput"
              }
            },
            "application/x-www-form-urlencoded": {
              "schema": {
                "$ref": "#/components/schemas/ContactInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Submission accepted"
          },
          "400": {
            "description": "Validation failed"
          },
          "403": {
            "description": "Origin not permitted"
          },
          "429": {
            "description": "Rate limit exceeded"
          }
        }
      }
    },
    "/api/newsletter": {
      "post": {
        "summary": "Subscribe to newsletter (double opt-in)",
        "operationId": "subscribeNewsletter",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/NewsletterInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Confirmation email sent"
          },
          "400": {
            "description": "Validation failed"
          },
          "403": {
            "description": "Origin not permitted"
          },
          "429": {
            "description": "Rate limit exceeded"
          }
        }
      }
    },
    "/api/reactions": {
      "get": {
        "summary": "Get reaction counts for a page",
        "operationId": "getReactions",
        "parameters": [
          {
            "name": "path",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Page path (e.g. /blog/some-post)"
          }
        ],
        "responses": {
          "200": {
            "description": "Reaction counts by type",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ReactionCounts"
                }
              }
            }
          },
          "400": {
            "description": "Missing or invalid path"
          }
        }
      }
    },
    "/api/mentions": {
      "get": {
        "summary": "Get approved webmentions for a page",
        "description": "Approved (moderated) webmentions for a page, plus the fediverse syndication URL for blog posts. Link-only and PII-free — source URL, hostname, and discovery time, never federated HTML content.",
        "operationId": "getMentions",
        "parameters": [
          {
            "name": "path",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Page path (e.g. /blog/some-post/)"
          }
        ],
        "responses": {
          "200": {
            "description": "Approved mentions and (for blog posts) syndication URL",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MentionsResponse"
                }
              }
            }
          },
          "400": {
            "description": "Missing or invalid path"
          },
          "500": {
            "description": "Failed to fetch mentions"
          }
        }
      }
    },
    "/api/pulse": {
      "get": {
        "summary": "Public, PII-free site pulse",
        "description": "Cached aggregate snapshot of the site: Core Web Vitals p75, pageview trend, uptime, top-reacted pages, and the latest deploy. Intentionally unauthenticated — no per-visitor or PII fields. Also available as the MCP `get_pulse` tool.",
        "operationId": "getPulse",
        "responses": {
          "200": {
            "description": "Site pulse snapshot",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PulseResponse"
                }
              }
            }
          },
          "500": {
            "description": "Failed to fetch pulse"
          }
        }
      }
    },
    "/api/agent-observatory": {
      "get": {
        "summary": "Public, PII-free agent-traffic observatory",
        "description": "Cached summary of AI-agent traffic to the site (MCP tool calls, llms.txt fetches, markdown-mirror reads, A2A requests): today's counts by type, top agent and tool over the trailing 7 days, a 14-day daily series, and a short PII-safe recent-event tail.",
        "operationId": "getAgentObservatory",
        "responses": {
          "200": {
            "description": "Agent-traffic observatory summary",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AgentObservatoryResponse"
                }
              }
            }
          },
          "500": {
            "description": "Failed to fetch agent observatory"
          }
        }
      }
    },
    "/api/webmention": {
      "post": {
        "summary": "Webmention receiver (W3C)",
        "description": "W3C Webmention endpoint (advertised via <link rel=\"webmention\">). Accepts application/x-www-form-urlencoded source/target parameters; the target must be a published content page on this site and the source document must actually link to it (verified synchronously).",
        "operationId": "receiveWebmention",
        "requestBody": {
          "required": true,
          "content": {
            "application/x-www-form-urlencoded": {
              "schema": {
                "$ref": "#/components/schemas/WebmentionInput"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Webmention verified and accepted"
          },
          "400": {
            "description": "Validation or verification failed"
          },
          "429": {
            "description": "Rate limit exceeded"
          },
          "500": {
            "description": "Failed to process webmention"
          }
        }
      }
    },
    "/mcp": {
      "post": {
        "summary": "Model Context Protocol endpoint (Streamable HTTP)",
        "description": "JSON-RPC entrypoint for MCP clients. DELETE is handled by the same Streamable HTTP transport; OPTIONS is answered as a CORS preflight before the transport is constructed. GET (the standalone SSE stream) is refused with 405 — the server is stateless, so nothing can ever push onto a GET-opened stream. See /.well-known/mcp/server-card.json for capabilities.",
        "operationId": "mcpRpc",
        "responses": {
          "200": {
            "description": "JSON-RPC response"
          }
        }
      },
      "get": {
        "summary": "Standalone SSE stream (not supported)",
        "description": "Refused with 405. This server is stateless (no session IDs), so a server-initiated SSE stream has no messages to carry and would only drive a client reconnect loop.",
        "operationId": "mcpSseStream",
        "responses": {
          "405": {
            "description": "Method Not Allowed; Allow: POST, DELETE, OPTIONS"
          }
        }
      }
    },
    "/a2a": {
      "post": {
        "summary": "A2A (Agent2Agent) protocol endpoint (JSON-RPC)",
        "description": "JSON-RPC entrypoint for A2A clients. GET returns the discovery document and OPTIONS handles CORS preflight. See /.well-known/agent-card.json for the declared interface.",
        "operationId": "a2aRpc",
        "responses": {
          "200": {
            "description": "JSON-RPC response"
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "HealthResponse": {
        "type": "object",
        "required": [
          "success",
          "status",
          "timestamp",
          "database"
        ],
        "properties": {
          "success": {
            "type": "boolean"
          },
          "status": {
            "type": "string",
            "enum": [
              "healthy"
            ]
          },
          "timestamp": {
            "type": "string",
            "format": "date-time"
          },
          "database": {
            "type": "string",
            "enum": [
              "connected"
            ]
          }
        }
      },
      "ContactInput": {
        "type": "object",
        "required": [
          "name",
          "email",
          "subject",
          "message"
        ],
        "properties": {
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 200
          },
          "email": {
            "type": "string",
            "format": "email",
            "maxLength": 320
          },
          "subject": {
            "type": "string",
            "minLength": 1,
            "maxLength": 300
          },
          "message": {
            "type": "string",
            "minLength": 1,
            "maxLength": 10000
          }
        }
      },
      "NewsletterInput": {
        "type": "object",
        "required": [
          "email"
        ],
        "properties": {
          "email": {
            "type": "string",
            "format": "email"
          },
          "source": {
            "type": "string"
          }
        }
      },
      "ReactionCounts": {
        "type": "object",
        "description": "Map of reaction type to count.",
        "additionalProperties": {
          "type": "integer",
          "minimum": 0
        }
      },
      "MentionsResponse": {
        "type": "object",
        "required": [
          "success",
          "mentions"
        ],
        "properties": {
          "success": {
            "type": "boolean"
          },
          "mentions": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "sourceUrl": {
                  "type": "string"
                },
                "sourceDomain": {
                  "type": "string"
                },
                "seenAt": {
                  "type": "string"
                }
              }
            }
          },
          "syndication": {
            "type": [
              "string",
              "null"
            ],
            "description": "Fediverse status URL for blog posts, else null."
          }
        }
      },
      "PulseResponse": {
        "type": "object",
        "required": [
          "success",
          "pulse"
        ],
        "properties": {
          "success": {
            "type": "boolean"
          },
          "pulse": {
            "type": "object",
            "required": [
              "asOf",
              "webVitals",
              "pageviews",
              "uptime",
              "topReacted",
              "deploy"
            ],
            "properties": {
              "asOf": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Latest data date (YYYY-MM-DD, UTC), or null."
              },
              "webVitals": {
                "type": "array",
                "items": {
                  "type": "object",
                  "required": [
                    "metric",
                    "p75",
                    "unit",
                    "rating"
                  ],
                  "properties": {
                    "metric": {
                      "type": "string",
                      "enum": [
                        "LCP",
                        "INP",
                        "CLS"
                      ]
                    },
                    "p75": {
                      "type": "number"
                    },
                    "unit": {
                      "type": "string",
                      "enum": [
                        "ms",
                        ""
                      ]
                    },
                    "rating": {
                      "type": "string",
                      "enum": [
                        "good",
                        "needs-improvement",
                        "poor"
                      ]
                    }
                  }
                }
              },
              "pageviews": {
                "type": "object",
                "required": [
                  "days",
                  "total",
                  "series"
                ],
                "properties": {
                  "days": {
                    "type": "integer"
                  },
                  "total": {
                    "type": "integer"
                  },
                  "series": {
                    "type": "array",
                    "items": {
                      "type": "integer"
                    },
                    "description": "Daily pageviews, oldest→newest."
                  }
                }
              },
              "uptime": {
                "type": [
                  "object",
                  "null"
                ],
                "properties": {
                  "pct": {
                    "type": "number"
                  },
                  "avgMs": {
                    "type": "number"
                  },
                  "days": {
                    "type": "integer"
                  }
                }
              },
              "topReacted": {
                "type": "array",
                "items": {
                  "type": "object",
                  "required": [
                    "path",
                    "count"
                  ],
                  "properties": {
                    "path": {
                      "type": "string"
                    },
                    "count": {
                      "type": "integer",
                      "minimum": 0
                    }
                  }
                }
              },
              "deploy": {
                "type": [
                  "object",
                  "null"
                ],
                "properties": {
                  "sha": {
                    "type": "string",
                    "description": "7-char short SHA"
                  },
                  "message": {
                    "type": "string"
                  },
                  "branch": {
                    "type": [
                      "string",
                      "null"
                    ]
                  },
                  "status": {
                    "type": "string"
                  },
                  "buildSeconds": {
                    "type": [
                      "integer",
                      "null"
                    ]
                  },
                  "completedAt": {
                    "type": "integer",
                    "description": "Epoch seconds."
                  }
                }
              }
            }
          }
        }
      },
      "AgentObservatoryResponse": {
        "type": "object",
        "required": [
          "success",
          "observatory"
        ],
        "properties": {
          "success": {
            "type": "boolean"
          },
          "observatory": {
            "type": "object",
            "required": [
              "asOf",
              "totalsToday",
              "topAgent",
              "topTool",
              "series",
              "recent"
            ],
            "properties": {
              "asOf": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Latest rollup or current date (YYYY-MM-DD, UTC), or null."
              },
              "totalsToday": {
                "type": "object",
                "required": [
                  "total",
                  "byType"
                ],
                "properties": {
                  "total": {
                    "type": "integer",
                    "minimum": 0
                  },
                  "byType": {
                    "type": "object",
                    "additionalProperties": {
                      "type": "integer",
                      "minimum": 0
                    }
                  }
                }
              },
              "topAgent": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "topTool": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "series": {
                "type": "array",
                "items": {
                  "type": "integer"
                },
                "description": "14-day daily event totals, oldest→newest."
              },
              "recent": {
                "type": "array",
                "description": "Most recent PII-safe agent events, newest first (≤15).",
                "items": {
                  "type": "object",
                  "required": [
                    "eventType",
                    "toolName",
                    "clientName",
                    "clientVersion",
                    "uaFamily",
                    "at"
                  ],
                  "properties": {
                    "eventType": {
                      "type": "string",
                      "enum": [
                        "mcp_tool",
                        "llms_txt",
                        "md_mirror",
                        "a2a"
                      ]
                    },
                    "toolName": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "clientName": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "clientVersion": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "uaFamily": {
                      "type": "string"
                    },
                    "at": {
                      "type": "integer",
                      "description": "Epoch seconds."
                    }
                  }
                }
              }
            }
          }
        }
      },
      "WebmentionInput": {
        "type": "object",
        "required": [
          "source",
          "target"
        ],
        "properties": {
          "source": {
            "type": "string",
            "format": "uri",
            "description": "URL of the source page that links to the target."
          },
          "target": {
            "type": "string",
            "format": "uri",
            "description": "URL on this site that the source links to."
          }
        }
      }
    }
  }
}