{
  "openapi": "3.1.0",
  "info": {
    "title": "The Meme Protocol API",
    "version": "1.1.0",
    "description": "Public API for listing approved memes and adding PNG/JPEG meme uploads. Uploads follow the same moderation, image validation, normalization, and IP quota rules as the browser form."
  },
  "servers": [
    {
      "url": "/"
    }
  ],
  "tags": [
    {
      "name": "memes",
      "description": "Approved meme discovery and addition"
    },
    {
      "name": "discovery",
      "description": "Machine-readable API metadata"
    }
  ],
  "paths": {
    "/api/memes": {
      "get": {
        "tags": [
          "memes"
        ],
        "summary": "List approved memes",
        "description": "Returns approved public memes only. Results are paginated and read-limited per IP to reduce abuse.",
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          },
          {
            "name": "pageSize",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 48,
              "default": 12
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Approved meme page",
            "headers": {
              "RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                }
              },
              "RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                }
              },
              "RateLimit-Reset": {
                "schema": {
                  "type": "string",
                  "format": "date-time"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MemeListResponse"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      },
      "post": {
        "tags": [
          "memes"
        ],
        "summary": "Add a meme",
        "description": "Adds one PNG or JPEG image using the same path as the browser upload form. The request must be multipart/form-data with a file field named meme. The server enforces the same size, dimension, pixel, moderation, normalization, and per-IP upload limits as browser additions.",
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": [
                  "meme"
                ],
                "properties": {
                  "meme": {
                    "type": "string",
                    "format": "binary",
                    "description": "PNG or JPEG image, maximum 5 MB, maximum edge 6000 px, maximum 20 megapixels."
                  }
                }
              },
              "encoding": {
                "meme": {
                  "contentType": "image/png, image/jpeg"
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Upload approved and published immediately",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MemeAddResponse"
                }
              }
            }
          },
          "202": {
            "description": "Upload accepted and queued for admin review",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MemeAddResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "411": {
            "$ref": "#/components/responses/Error"
          },
          "413": {
            "$ref": "#/components/responses/Error"
          },
          "415": {
            "$ref": "#/components/responses/Error"
          },
          "422": {
            "description": "Upload rejected by moderation",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ModerationRejected"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/memes/{id}": {
      "get": {
        "tags": [
          "memes"
        ],
        "summary": "Get one approved meme",
        "description": "Returns public metadata for an approved meme. Pending, rejected, deleted, or unknown IDs return 404.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^[a-f0-9]{64}$"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Approved meme metadata",
            "headers": {
              "RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                }
              },
              "RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                }
              },
              "RateLimit-Reset": {
                "schema": {
                  "type": "string",
                  "format": "date-time"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "meme",
                    "rateLimit"
                  ],
                  "properties": {
                    "meme": {
                      "$ref": "#/components/schemas/Meme"
                    },
                    "rateLimit": {
                      "$ref": "#/components/schemas/RateLimit"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/Error"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/media/{id}": {
      "get": {
        "tags": [
          "memes"
        ],
        "summary": "Fetch approved meme image",
        "description": "Returns the normalized WebP image for an approved meme. Media reads are rate-limited per IP.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^[a-f0-9]{64}$"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "WebP meme image",
            "content": {
              "image/webp": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "404": {
            "description": "Not found"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/openapi.json": {
      "get": {
        "tags": [
          "discovery"
        ],
        "summary": "OpenAPI document",
        "responses": {
          "200": {
            "description": "OpenAPI 3.1 document",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/meme-api.skill.md": {
      "get": {
        "tags": [
          "discovery"
        ],
        "summary": "LLM API consumption instructions",
        "responses": {
          "200": {
            "description": "Markdown skill instructions for LLM clients",
            "content": {
              "text/markdown": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "Meme": {
        "type": "object",
        "required": [
          "id",
          "createdAt",
          "byteSize",
          "width",
          "height",
          "mime",
          "originalMime",
          "status",
          "moderationScore",
          "moderationReason",
          "viewCount",
          "downloadCount",
          "originalName",
          "url",
          "downloadUrl",
          "demo"
        ],
        "properties": {
          "id": {
            "type": "string",
            "pattern": "^[a-f0-9]{64}$"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "byteSize": {
            "type": "integer",
            "minimum": 0
          },
          "width": {
            "type": "integer",
            "minimum": 1
          },
          "height": {
            "type": "integer",
            "minimum": 1
          },
          "mime": {
            "type": "string",
            "const": "image/webp"
          },
          "originalMime": {
            "type": "string",
            "enum": [
              "image/png",
              "image/jpeg"
            ]
          },
          "status": {
            "type": "string",
            "enum": [
              "approved",
              "pending"
            ]
          },
          "moderationScore": {
            "type": "integer",
            "minimum": 0,
            "maximum": 100
          },
          "moderationReason": {
            "type": "string"
          },
          "viewCount": {
            "type": "integer",
            "minimum": 0
          },
          "downloadCount": {
            "type": "integer",
            "minimum": 0
          },
          "originalName": {
            "type": "string"
          },
          "url": {
            "type": "string",
            "format": "uri-reference"
          },
          "downloadUrl": {
            "type": "string",
            "format": "uri-reference"
          },
          "demo": {
            "type": "boolean"
          }
        }
      },
      "MemeListResponse": {
        "type": "object",
        "required": [
          "page",
          "pageSize",
          "total",
          "totalPages",
          "memes",
          "rateLimit"
        ],
        "properties": {
          "page": {
            "type": "integer"
          },
          "pageSize": {
            "type": "integer"
          },
          "total": {
            "type": "integer"
          },
          "totalPages": {
            "type": "integer"
          },
          "memes": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Meme"
            }
          },
          "rateLimit": {
            "$ref": "#/components/schemas/RateLimit"
          }
        }
      },
      "MemeAddResponse": {
        "type": "object",
        "required": [
          "meme",
          "quota",
          "moderationScore",
          "moderationReason",
          "message"
        ],
        "properties": {
          "meme": {
            "$ref": "#/components/schemas/Meme"
          },
          "quota": {
            "$ref": "#/components/schemas/UploadQuota"
          },
          "moderationScore": {
            "type": "integer",
            "minimum": 0,
            "maximum": 100
          },
          "moderationReason": {
            "type": "string"
          },
          "message": {
            "type": "string",
            "enum": [
              "Upload approved.",
              "Upload queued for admin review."
            ]
          }
        }
      },
      "UploadQuota": {
        "type": "object",
        "required": [
          "remainingHour",
          "remainingDay",
          "remainingGlobalDay"
        ],
        "properties": {
          "remainingHour": {
            "type": "integer",
            "minimum": 0
          },
          "remainingDay": {
            "type": "integer",
            "minimum": 0
          },
          "remainingGlobalDay": {
            "type": "integer",
            "minimum": 0
          }
        }
      },
      "RateLimit": {
        "type": "object",
        "required": [
          "limit",
          "remaining",
          "resetAt"
        ],
        "properties": {
          "limit": {
            "type": "integer"
          },
          "remaining": {
            "type": "integer"
          },
          "resetAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "Error": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "string"
          }
        }
      },
      "ModerationRejected": {
        "type": "object",
        "required": [
          "error",
          "moderationScore",
          "moderationReason"
        ],
        "properties": {
          "error": {
            "type": "string"
          },
          "moderationScore": {
            "type": "integer",
            "minimum": 0,
            "maximum": 100
          },
          "moderationReason": {
            "type": "string"
          }
        }
      }
    },
    "responses": {
      "Error": {
        "description": "Error response",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "RateLimited": {
        "description": "Rate limit exceeded",
        "headers": {
          "Retry-After": {
            "schema": {
              "type": "integer",
              "minimum": 1
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      }
    }
  }
}
