{
  "openapi": "3.1.0",
  "info": {
    "title": "Tuppence API",
    "version": "1.0.0",
    "summary": "API pubblica in sola lettura su budget personale, spese e calcolatori.",
    "description": "Espone i contenuti di tuppenceapp.app: 22 guide su budget, spese, risparmio e abbonamenti, i metodi di ripartizione del reddito con la loro fonte e 4 calcolatori eseguibili via query string. Nessuna autenticazione. Limite: 60 richieste ogni 60 secondi per IP, applicato per istanza edge (best-effort) e comunicato con gli header IETF RateLimit e RateLimit-Policy. Gli errori sono in application/problem+json (RFC 9457). Lo stesso contenuto è disponibile come server MCP su https://tuppenceapp.app/mcp. Tuppence è un progetto indipendente: non è una banca, non è un intermediario finanziario e non offre consulenza finanziaria. I contenuti di questo sito hanno finalità divulgativa.",
    "contact": {
      "name": "Simone Ruggiero",
      "email": "simone.ruggiero97@gmail.com",
      "url": "https://tuppenceapp.app/contact/"
    }
  },
  "externalDocs": {
    "description": "Documentazione",
    "url": "https://tuppenceapp.app/docs/"
  },
  "servers": [
    {
      "url": "https://tuppenceapp.app"
    }
  ],
  "security": [],
  "tags": [
    {
      "name": "Sito"
    },
    {
      "name": "Guide"
    },
    {
      "name": "Calcolatori"
    }
  ],
  "paths": {
    "/api/v1": {
      "get": {
        "operationId": "getApiIndex",
        "tags": [
          "Sito"
        ],
        "summary": "Indice dell'API",
        "description": "Elenco degli endpoint disponibili e link a documentazione, OpenAPI e MCP.",
        "responses": {
          "200": {
            "description": "Indice",
            "headers": {
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiIndex"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/Problem"
          }
        }
      }
    },
    "/api/v1/info": {
      "get": {
        "operationId": "getSiteInfo",
        "tags": [
          "Sito"
        ],
        "summary": "Informazioni sul sito",
        "description": "Descrizione del sito, app iOS collegata, autore, contatti e numero di contenuti.",
        "responses": {
          "200": {
            "description": "Informazioni",
            "headers": {
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SiteInfo"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/Problem"
          }
        }
      }
    },
    "/api/v1/methods": {
      "get": {
        "operationId": "getBudgetMethods",
        "tags": [
          "Sito"
        ],
        "summary": "Metodi di budget",
        "description": "Metodi di ripartizione del reddito con quote, cosa copre ognuna e fonte, più l'intervallo di riferimento del fondo di emergenza.",
        "responses": {
          "200": {
            "description": "Metodi",
            "headers": {
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BudgetMethods"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/Problem"
          }
        }
      }
    },
    "/api/v1/guides": {
      "get": {
        "operationId": "listGuides",
        "tags": [
          "Guide"
        ],
        "summary": "Elenca o cerca le guide",
        "description": "Restituisce le guide pubblicate, filtrabili per argomento e ricerca testuale su titolo, descrizione e keyword.",
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": false,
            "description": "Parole chiave (max 100 caratteri).",
            "schema": {
              "type": "string",
              "maxLength": 100
            }
          },
          {
            "name": "cluster",
            "in": "query",
            "required": false,
            "description": "Argomento.",
            "schema": {
              "type": "string",
              "enum": [
                "Budget",
                "Spese",
                "Risparmio",
                "Abbonamenti"
              ]
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Numero massimo di risultati.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 100
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Guide trovate",
            "headers": {
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GuideList"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Problem"
          },
          "429": {
            "$ref": "#/components/responses/Problem"
          }
        }
      }
    },
    "/api/v1/guides/{slug}": {
      "get": {
        "operationId": "getGuide",
        "tags": [
          "Guide"
        ],
        "summary": "Leggi una guida",
        "description": "Metadati e testo completo in markdown di una guida.",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "description": "Slug della guida, es. metodo-50-30-20.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9-]{1,120}$"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Guida",
            "headers": {
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Guide"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Problem"
          },
          "404": {
            "$ref": "#/components/responses/Problem"
          },
          "429": {
            "$ref": "#/components/responses/Problem"
          }
        }
      }
    },
    "/api/v1/calculators": {
      "get": {
        "operationId": "listCalculators",
        "tags": [
          "Calcolatori"
        ],
        "summary": "Elenca i calcolatori",
        "description": "Calcolatori disponibili, con gli input che accettano e a cosa rispondono.",
        "responses": {
          "200": {
            "description": "Calcolatori",
            "headers": {
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CalculatorList"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/Problem"
          }
        }
      }
    },
    "/api/v1/calculators/{slug}": {
      "get": {
        "operationId": "runCalculator",
        "tags": [
          "Calcolatori"
        ],
        "summary": "Esegui un calcolatore",
        "description": "Esegue il calcolo sugli importi passati in query string. Senza parametri restituisce la descrizione del calcolatore. Gli slug validi sono: 50-30-20, budget-mensile, abbonamenti, fondo-emergenza. Per \"abbonamenti\" si ripete il parametro item nella forma importo:cadenza, es. item=9.99:monthly&item=59:yearly; le cadenze sono daily, weekly, monthly, quarterly, yearly.",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "description": "Slug del calcolatore, es. 50-30-20.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9-]{1,120}$"
            }
          },
          {
            "name": "monthlyNet",
            "in": "query",
            "required": false,
            "description": "Reddito netto mensile (calcolatore 50-30-20).",
            "schema": {
              "type": "number",
              "minimum": 0
            }
          },
          {
            "name": "income",
            "in": "query",
            "required": false,
            "description": "Entrate del periodo (calcolatore budget-mensile).",
            "schema": {
              "type": "number",
              "minimum": 0
            }
          },
          {
            "name": "fixedCosts",
            "in": "query",
            "required": false,
            "description": "Spese fisse già impegnate (calcolatore budget-mensile).",
            "schema": {
              "type": "number",
              "minimum": 0
            }
          },
          {
            "name": "daysLeft",
            "in": "query",
            "required": false,
            "description": "Giorni che mancano alla fine del periodo (calcolatore budget-mensile).",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 366
            }
          },
          {
            "name": "monthlyEssentials",
            "in": "query",
            "required": false,
            "description": "Spese essenziali mensili (calcolatore fondo-emergenza).",
            "schema": {
              "type": "number",
              "minimum": 0
            }
          },
          {
            "name": "irregularIncome",
            "in": "query",
            "required": false,
            "description": "Reddito irregolare (calcolatore fondo-emergenza).",
            "schema": {
              "type": "boolean"
            }
          },
          {
            "name": "item",
            "in": "query",
            "required": false,
            "description": "Voce ricorrente nella forma importo:cadenza, ripetibile (calcolatore abbonamenti).",
            "schema": {
              "type": "array",
              "items": {
                "type": "string"
              }
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Risultato",
            "headers": {
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CalculatorResult"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Problem"
          },
          "404": {
            "$ref": "#/components/responses/Problem"
          },
          "429": {
            "$ref": "#/components/responses/Problem"
          }
        }
      }
    }
  },
  "components": {
    "headers": {
      "RateLimit": {
        "description": "Quota residua e secondi al reset (draft-ietf-httpapi-ratelimit-headers).",
        "schema": {
          "type": "string",
          "examples": [
            "\"default\";r=59;t=60"
          ]
        }
      },
      "RateLimitPolicy": {
        "description": "Politica: q richieste per finestra di w secondi.",
        "schema": {
          "type": "string",
          "examples": [
            "\"default\";q=60;w=60"
          ]
        }
      }
    },
    "responses": {
      "Problem": {
        "description": "Errore (RFC 9457)",
        "headers": {
          "RateLimit": {
            "$ref": "#/components/headers/RateLimit"
          },
          "RateLimit-Policy": {
            "$ref": "#/components/headers/RateLimitPolicy"
          }
        },
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      }
    },
    "schemas": {
      "Problem": {
        "type": "object",
        "required": [
          "type",
          "title",
          "status",
          "detail",
          "code",
          "resolution"
        ],
        "properties": {
          "type": {
            "type": "string",
            "format": "uri"
          },
          "title": {
            "type": "string"
          },
          "status": {
            "type": "integer"
          },
          "detail": {
            "type": "string"
          },
          "code": {
            "type": "string",
            "enum": [
              "invalid_query",
              "invalid_cluster",
              "invalid_limit",
              "invalid_slug",
              "invalid_amount",
              "guide_not_found",
              "calculator_not_found",
              "not_found",
              "method_not_allowed",
              "rate_limited"
            ],
            "description": "Codice macchina, es. guide_not_found, rate_limited."
          },
          "resolution": {
            "type": "string",
            "description": "Cosa fare per risolvere."
          }
        }
      },
      "ApiIndex": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string"
          },
          "version": {
            "type": "string"
          },
          "documentation": {
            "type": "string",
            "format": "uri"
          },
          "openapi": {
            "type": "string",
            "format": "uri"
          },
          "mcp": {
            "type": "string",
            "format": "uri"
          },
          "endpoints": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "SiteInfo": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string"
          },
          "url": {
            "type": "string",
            "format": "uri"
          },
          "apiVersion": {
            "type": "string"
          },
          "language": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "app": {
            "type": "object",
            "properties": {
              "name": {
                "type": "string"
              },
              "platform": {
                "type": "string"
              },
              "minOsVersion": {
                "type": "string"
              },
              "appStoreUrl": {
                "type": "string",
                "format": "uri"
              },
              "languages": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              }
            }
          },
          "author": {
            "type": "object",
            "properties": {
              "name": {
                "type": "string"
              },
              "url": {
                "type": "string",
                "format": "uri"
              }
            }
          },
          "contact": {
            "type": "object",
            "properties": {
              "email": {
                "type": "string",
                "format": "email"
              },
              "page": {
                "type": "string",
                "format": "uri"
              }
            }
          },
          "location": {
            "type": "object",
            "properties": {
              "city": {
                "type": "string"
              },
              "country": {
                "type": "string"
              }
            }
          },
          "disclaimer": {
            "type": "string"
          },
          "links": {
            "type": "object",
            "additionalProperties": {
              "type": "string",
              "format": "uri"
            }
          },
          "guides": {
            "type": "integer"
          },
          "calculators": {
            "type": "integer"
          }
        }
      },
      "BudgetSlice": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "share": {
            "type": "number"
          },
          "covers": {
            "type": "string"
          }
        }
      },
      "BudgetMethods": {
        "type": "object",
        "properties": {
          "methods": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string"
                },
                "name": {
                  "type": "string"
                },
                "summary": {
                  "type": "string"
                },
                "source": {
                  "type": "string",
                  "description": "Da dove viene il metodo."
                },
                "slices": {
                  "type": [
                    "array",
                    "null"
                  ],
                  "items": {
                    "$ref": "#/components/schemas/BudgetSlice"
                  }
                },
                "guide": {
                  "type": "string",
                  "format": "uri"
                }
              }
            }
          },
          "emergencyFund": {
            "type": "object",
            "properties": {
              "minMonths": {
                "type": "integer"
              },
              "maxMonths": {
                "type": "integer"
              },
              "irregularIncomeMonths": {
                "type": "integer"
              },
              "basis": {
                "type": "string"
              }
            }
          },
          "note": {
            "type": "string"
          }
        }
      },
      "GuideSummary": {
        "type": "object",
        "required": [
          "slug",
          "title",
          "url"
        ],
        "properties": {
          "slug": {
            "type": "string"
          },
          "title": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "cluster": {
            "type": "string",
            "enum": [
              "Budget",
              "Spese",
              "Risparmio",
              "Abbonamenti"
            ]
          },
          "keywords": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "date": {
            "type": "string",
            "format": "date"
          },
          "updatedAt": {
            "type": "string",
            "format": "date"
          },
          "url": {
            "type": "string",
            "format": "uri"
          },
          "markdownUrl": {
            "type": "string",
            "format": "uri"
          }
        }
      },
      "Guide": {
        "allOf": [
          {
            "$ref": "#/components/schemas/GuideSummary"
          },
          {
            "type": "object",
            "properties": {
              "markdown": {
                "type": "string",
                "description": "Testo completo in markdown."
              }
            }
          }
        ]
      },
      "GuideList": {
        "type": "object",
        "properties": {
          "total": {
            "type": "integer"
          },
          "guides": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/GuideSummary"
            }
          }
        }
      },
      "Calculator": {
        "type": "object",
        "required": [
          "slug",
          "title"
        ],
        "properties": {
          "slug": {
            "type": "string"
          },
          "title": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "answers": {
            "type": "string"
          },
          "inputs": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Parametri accettati in query string."
          },
          "url": {
            "type": "string",
            "format": "uri"
          }
        }
      },
      "CalculatorList": {
        "type": "object",
        "properties": {
          "total": {
            "type": "integer"
          },
          "calculators": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Calculator"
            }
          }
        }
      },
      "CalculatorResult": {
        "type": "object",
        "properties": {
          "calculator": {
            "type": "string"
          },
          "url": {
            "type": "string",
            "format": "uri"
          },
          "result": {
            "type": "object",
            "description": "Struttura dipendente dal calcolatore.",
            "additionalProperties": true
          },
          "disclaimer": {
            "type": "string"
          }
        }
      }
    }
  }
}