{
  "openapi": "3.1.0",
  "info": {
    "title": "fries.wtf — fast food value index",
    "summary": "Nutrition-per-dollar rankings and an exact meal-cost optimiser across 33 US fast-food chains and 13 metro areas.",
    "description": "Returns the derived index — value and health scores, protein per dollar, and solved cheapest-meal plans — not raw price lists.\n\nNutrition figures are the chains' own published values. Prices are collected per metro and vary by location, so per-dollar metrics are estimates for comparison rather than quotes.\n\nIndependent. Not affiliated with, endorsed by, or sponsored by any restaurant company. Chain names are trademarks of their respective owners, used only to identify their menu items.",
    "version": "1.0.0",
    "contact": { "name": "fries.wtf", "url": "https://fries.wtf/developers.php" }
  },
  "servers": [{ "url": "https://fries.wtf", "description": "Production" }],
  "security": [{ "apiKeyQuery": [] }, { "bearerAuth": [] }],
  "tags": [
    { "name": "index", "description": "Rankings and coverage" },
    { "name": "optimiser", "description": "Cheapest-meal solving" }
  ],
  "paths": {
    "/v1.php": {
      "get": {
        "operationId": "query",
        "tags": ["index", "optimiser"],
        "summary": "Rankings, meal plans, or chain coverage",
        "description": "A single entry point selected by the `endpoint` parameter.\n\n- `rankings` — items ordered by a chosen metric, with the full index attached.\n- `plan` — cheapest combination of real menu items reaching a protein target within a budget and calorie cap, solved exactly per chain.\n- `chains` — coverage list with item counts.",
        "parameters": [
          {
            "name": "endpoint", "in": "query", "required": true,
            "description": "Which operation to run.",
            "schema": { "type": "string", "enum": ["rankings", "plan", "chains"], "default": "rankings" }
          },
          {
            "name": "region", "in": "query",
            "description": "Metro area. `US` is the national default menu.",
            "schema": { "type": "string", "enum": ["US","NYC","CHI","LA","SF","ATL","HOU","PHX","SEA","DEN","MIA","MSP","SGF"], "default": "US" }
          },
          {
            "name": "metric", "in": "query",
            "description": "Sort key. `rankings` only.",
            "schema": { "type": "string", "enum": ["value","protein_per_dollar","calories_per_dollar","health","protein_density"], "default": "value" }
          },
          {
            "name": "chain", "in": "query",
            "description": "Restrict to one chain id, e.g. `panera`. Call `endpoint=chains` for valid ids.",
            "schema": { "type": "string" }
          },
          {
            "name": "limit", "in": "query",
            "description": "Results to return. Up to 250 for rankings, 40 for plans.",
            "schema": { "type": "integer", "minimum": 1, "maximum": 250, "default": 50 }
          },
          {
            "name": "min_calories", "in": "query",
            "description": "Quality floor. Defaults to 100 so condiment cups do not dominate ratio metrics. Set 0 to disable.",
            "schema": { "type": "integer", "minimum": 0, "default": 100 }
          },
          {
            "name": "min_protein", "in": "query",
            "description": "Quality floor in grams. Defaults to 5 for `protein_per_dollar` and `protein_density`, 0 for every other metric. Set 0 to disable.",
            "schema": { "type": "number", "minimum": 0 }
          },
          {
            "name": "protein", "in": "query",
            "description": "Protein target in grams. Required when `endpoint=plan`.",
            "schema": { "type": "number", "minimum": 1, "maximum": 500 }
          },
          {
            "name": "budget", "in": "query",
            "description": "Maximum total spend in USD. `plan` only. Omit for no limit.",
            "schema": { "type": "number", "exclusiveMinimum": 0 }
          },
          {
            "name": "calories", "in": "query",
            "description": "Maximum total calories. `plan` only. Omit for no limit.",
            "schema": { "type": "number", "exclusiveMinimum": 0 }
          },
          {
            "name": "max_per_item", "in": "query",
            "description": "Cap on repeats of any single item in a plan. Defaults to 2 — the unconstrained optimum is often N copies of one cheap item, which is correct but not edible.",
            "schema": { "type": "integer", "minimum": 1, "maximum": 8, "default": 2 }
          },
          {
            "name": "include_treats", "in": "query",
            "description": "Allow drinks and desserts in plans.",
            "schema": { "type": "boolean", "default": false }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    { "$ref": "#/components/schemas/RankingsResponse" },
                    { "$ref": "#/components/schemas/PlanResponse" },
                    { "$ref": "#/components/schemas/ChainsResponse" }
                  ]
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/Error" },
          "401": { "$ref": "#/components/responses/Error" },
          "403": { "$ref": "#/components/responses/Error" },
          "404": { "$ref": "#/components/responses/Error" },
          "429": { "$ref": "#/components/responses/Error" }
        }
      }
    },
    "/mcp.php": {
      "post": {
        "operationId": "mcp",
        "summary": "Model Context Protocol endpoint",
        "description": "JSON-RPC 2.0 over HTTP for AI agents. `initialize` and `tools/list` work without a key so a client can inspect the server before subscribing; `tools/call` requires one.\n\nTools: `fastfood_rankings`, `fastfood_plan`, `fastfood_chains`.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["jsonrpc", "method"],
                "properties": {
                  "jsonrpc": { "type": "string", "const": "2.0" },
                  "id": { "type": ["string", "integer", "null"] },
                  "method": { "type": "string", "examples": ["initialize", "tools/list", "tools/call"] },
                  "params": { "type": "object" }
                }
              }
            }
          }
        },
        "responses": {
          "200": { "description": "JSON-RPC result or error", "content": { "application/json": { "schema": { "type": "object" } } } },
          "204": { "description": "Notification accepted, no body" }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "apiKeyQuery": { "type": "apiKey", "in": "query", "name": "key" },
      "bearerAuth": { "type": "http", "scheme": "bearer" }
    },
    "responses": {
      "Error": {
        "description": "Request refused",
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "properties": {
                "error": { "type": "string", "examples": ["invalid_key", "quota_exceeded", "bad_metric", "no_data"] },
                "detail": { "type": "string" }
              }
            }
          }
        }
      }
    },
    "schemas": {
      "Item": {
        "type": "object",
        "description": "One menu item with its derived index. Raw price is deliberately not included.",
        "properties": {
          "id": { "type": "integer" },
          "rank": { "type": "integer" },
          "name": { "type": "string" },
          "chain": { "type": "string" },
          "chain_name": { "type": "string" },
          "category": { "type": ["string", "null"] },
          "calories": { "type": "integer" },
          "protein_g": { "type": "number" },
          "protein_per_dollar": { "type": "number", "description": "Grams of protein per USD." },
          "calories_per_dollar": { "type": "number" },
          "protein_density": { "type": "number", "description": "Grams of protein per 100 kcal." },
          "balance_score": { "type": "integer", "minimum": 0, "maximum": 100 },
          "health_score": { "type": ["integer", "null"], "minimum": 0, "maximum": 100 },
          "value_score": { "type": "integer", "minimum": 0, "maximum": 100, "description": "45% protein-per-dollar percentile, 15% calories-per-dollar, 40% health." }
        }
      },
      "RankingsResponse": {
        "type": "object",
        "properties": {
          "generated_at": { "type": "string", "format": "date-time" },
          "region": { "type": "string" },
          "metric": { "type": "string" },
          "count": { "type": "integer" },
          "pool_size": { "type": "integer", "description": "Items considered before limiting — percentiles are computed against this whole pool." },
          "attribution": { "type": "string" },
          "items": { "type": "array", "items": { "$ref": "#/components/schemas/Item" } }
        }
      },
      "PlanResponse": {
        "type": "object",
        "properties": {
          "generated_at": { "type": "string", "format": "date-time" },
          "region": { "type": "string" },
          "constraints": { "type": "object" },
          "count": { "type": "integer" },
          "attribution": { "type": "string" },
          "plans": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "chain": { "type": "string" },
                "chain_name": { "type": "string" },
                "combo": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "properties": {
                      "id": { "type": "integer" },
                      "name": { "type": "string" },
                      "qty": { "type": "integer" },
                      "calories_each": { "type": "integer" },
                      "protein_g_each": { "type": "number" }
                    }
                  }
                },
                "totals": {
                  "type": "object",
                  "properties": {
                    "cost": { "type": "number", "description": "Total USD for the basket. An optimiser result, not a price list." },
                    "calories": { "type": "integer" },
                    "protein": { "type": "number" },
                    "carbs": { "type": "number" },
                    "fat": { "type": "number" },
                    "sodium": { "type": "integer" }
                  }
                }
              }
            }
          },
          "unreachable": {
            "type": "array",
            "description": "Chains that could not reach the target within the constraints, with the reason.",
            "items": { "type": "object", "properties": { "chain": { "type": "string" }, "reason": { "type": "string" } } }
          }
        }
      },
      "ChainsResponse": {
        "type": "object",
        "properties": {
          "generated_at": { "type": "string", "format": "date-time" },
          "chains": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": { "type": "string" },
                "name": { "type": "string" },
                "type": { "type": ["string", "null"] },
                "items": { "type": "integer" }
              }
            }
          }
        }
      }
    }
  }
}
