{
  "openapi": "3.1.1",
  "info": {
    "title": "Everyday calculators API",
    "version": "1.0.0",
    "summary": "Deterministic tools for people and AI agents.",
    "description": "Every endpoint performs the same calculation as the human page and returns inputs, result, units, assumptions, sources, warnings and computedAt. MCP endpoint: /mcp."
  },
  "servers": [
    {
      "url": "https://calculators.usefulweb.site"
    }
  ],
  "paths": {
    "/api/v1/tools": {
      "get": {
        "operationId": "listTools",
        "summary": "Catalog of all tools",
        "responses": {
          "200": {
            "description": "Tool list.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/tools/{id}": {
      "get": {
        "operationId": "getTool",
        "summary": "Definition of one tool",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "loan-amortization-calculator",
                "one-rep-max-calculator",
                "percentage-ratio-calculator",
                "random-generator",
                "running-pace-calculator",
                "sports-travel-fatigue-estimator",
                "tip-bill-split-calculator"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Tool definition.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "404": {
            "description": "Invalid input (see errors[]).",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/tools/loan-amortization-calculator/calculate": {
      "post": {
        "operationId": "calculateLoanAmortizationCalculator",
        "summary": "Annuity, linear or interest-only loans: schedule, extra payments, fixed-rate balance, APR.",
        "description": "Calculates the payment and the complete amortization schedule of an annuity, linear (constant principal) or interest-only loan, including one-off and recurring extra payments, the remaining balance at the end of a fixed-rate period, the effective annual rate and the APR with up-front fees. Download the schedule as CSV.\n\nMethodology:\n- Rate per period i = nominal annual rate ÷ payments per year (the usual convention for German and most European loans).\n- Annuity payment A = L·i ÷ (1 − (1 + i)^−n), rounded to the cent; with an initial repayment rate t instead: A = L × (rate + t) ÷ payments per year, and the term follows from A.\n- Each period: interest = balance × i (rounded to the cent); principal = A − interest (annuity), L ÷ n (linear) or 0 until the last payment (interest-only). The last scheduled payment clears any remaining cents.\n- Extra payments reduce the balance on top of the regular payment. Annuity loans keep the payment and finish earlier; linear and interest-only loans keep their principal plan and pay less interest.\n- Remaining balance at the end of the fixed-rate period = balance after the last payment inside that period.\n- Effective annual rate (no fees) = (1 + i)^m − 1. APR: the rate X solving Σ payment_l × (1 + X)^(−t_l) = loan − up-front fees, with t_l measured in years of 12 standard months, as in the equation of the PAngV annex (EU Consumer Credit Directive). This is an approximation: real offers count exact days, may add account fees or insurance, and lenders round to two decimals.",
        "tags": [
          "calculators"
        ],
        "x-mcp-tool": "loan_amortization_calculator",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/LoanAmortizationCalculatorInput"
              },
              "examples": {
                "example1": {
                  "summary": "Mortgage 300,000 at 3.8%, 2% initial repayment, 10-year fixed rate",
                  "value": {
                    "principal": 300000,
                    "currency": "EUR",
                    "ratePercent": 3.8,
                    "loanType": "annuity",
                    "initialRepaymentPercent": 2,
                    "frequency": "monthly",
                    "startDate": "2026-11-01",
                    "fixedRateYears": 10,
                    "upfrontFees": 0,
                    "recurringExtra": 5000,
                    "recurringEvery": 12,
                    "recurringFrom": 12
                  }
                },
                "example2": {
                  "summary": "Car loan 20,000, 5 years at 6.9% with 400 fees",
                  "value": {
                    "principal": 20000,
                    "currency": "USD",
                    "ratePercent": 6.9,
                    "loanType": "annuity",
                    "termYears": 5,
                    "frequency": "monthly",
                    "upfrontFees": 400
                  }
                },
                "example3": {
                  "summary": "Linear loan 120,000 over 10 years, quarterly",
                  "value": {
                    "principal": 120000,
                    "ratePercent": 4,
                    "loanType": "linear",
                    "termYears": 10,
                    "frequency": "quarterly",
                    "startDate": "2027-01-01",
                    "extraPayments": [
                      {
                        "period": 8,
                        "amount": 10000
                      }
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Calculation result envelope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LoanAmortizationCalculatorResult"
                }
              }
            }
          },
          "400": {
            "description": "Malformed JSON body.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "Invalid input (see errors[]).",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded (see Retry-After).",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/tools/one-rep-max-calculator/calculate": {
      "post": {
        "operationId": "calculateOneRepMaxCalculator",
        "summary": "1RM from any set (7 formulas + average), % and RPE tables, and which plates to load.",
        "description": "Estimates your one-repetition maximum from the weight and reps of a set to failure (or with an RPE) with the Epley, Brzycki, Lander, Lombardi, Mayhew, O'Conner and Wathan equations and their average, prints a percentage table and an RPE table, and tells you which plates to put on each side of a kg or lb bar using only the plates you actually have.\n\nMethodology:\n- Seven published equations estimate the 1RM from the weight w and repetitions r to failure, as tabulated by LeSuer et al. (1997): Epley w(1 + 0.0333r); Brzycki 100w/(102.78 − 2.78r); Lander 100w/(101.3 − 2.67123r); Lombardi w·r^0.10; Mayhew 100w/(52.2 + 41.9e^(−0.055r)); O'Conner w(1 + 0.025r); Wathan 100w/(48.8 + 53.8e^(−0.075r)). The average of all seven is the headline estimate.\n- With 1 rep (and no RPE) the 1RM is the weight lifted.\n- RPE: reps in reserve = 10 − RPE are added to the reps (e.g. 5 reps at RPE 8 count as 7 reps to failure). The RPE table uses the same idea with the Epley relation %1RM = 1 ÷ (1 + 0.0333 × (reps + 10 − RPE)); it is derived, not a copy of a published RPE chart.\n- Percentage table: weight = % × average 1RM; the approximate reps at each percentage invert the Epley equation: reps = (1/% − 1) ÷ 0.0333.\n- Plate loader: per side = (target − bar − collars) ÷ 2. Plates are taken heaviest first (greedy) from the pairs you have; if that misses but an exact combination exists (possible with unusual plate sets), an exact subset-sum search finds the combination with the fewest plates. If the weight cannot be loaded exactly, the nearest loadable weights below and above are shown.",
        "tags": [
          "calculators"
        ],
        "x-mcp-tool": "one_rep_max_calculator",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/OneRepMaxCalculatorInput"
              },
              "examples": {
                "example1": {
                  "summary": "100 kg × 5 reps, load 85%",
                  "value": {
                    "weight": 100,
                    "reps": 5,
                    "unit": "kg",
                    "targetPercent": 85
                  }
                },
                "example2": {
                  "summary": "225 lb × 8 at RPE 8, load 275 lb",
                  "value": {
                    "weight": 225,
                    "reps": 8,
                    "unit": "lb",
                    "rpe": 8,
                    "targetWeight": 275
                  }
                },
                "example3": {
                  "summary": "Home gym: 62.5 kg with a 15 kg bar and few plates",
                  "value": {
                    "weight": 50,
                    "reps": 10,
                    "unit": "kg",
                    "targetWeight": 62.5,
                    "barWeight": 15,
                    "plates": [
                      {
                        "weight": 10,
                        "count": 2
                      },
                      {
                        "weight": 5,
                        "count": 4
                      },
                      {
                        "weight": 2.5,
                        "count": 2
                      },
                      {
                        "weight": 1.25,
                        "count": 2
                      }
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Calculation result envelope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OneRepMaxCalculatorResult"
                }
              }
            }
          },
          "400": {
            "description": "Malformed JSON body.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "Invalid input (see errors[]).",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded (see Retry-After).",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/tools/percentage-ratio-calculator/calculate": {
      "post": {
        "operationId": "calculatePercentageRatioCalculator",
        "summary": "X% of Y, % change, reverse %, percentage points, ratios and aspect ratios, with formulas.",
        "description": "Answers every everyday percentage question (X% of Y, X is what percent of Y, percentage change, increase or decrease by a percentage, reverse percentage, percentage points versus percent) and every ratio task (simplify, scale, split an amount, aspect ratios). Each answer shows the formula and the steps.\n\nMethodology:\n- X% of Y = X ÷ 100 × Y. X as a percent of Y = X ÷ Y × 100.\n- Percentage change from X to Y = (Y − X) ÷ |X| × 100; positive is an increase.\n- Increase Y by X% = Y × (1 + X/100); decrease Y by X% = Y × (1 − X/100).\n- Reverse percentage: if Y is the value after a change of X%, the original is Y ÷ (1 + X/100) (use a negative X for a decrease, e.g. −20 for 20% off).\n- Percentage points measure the plain difference between two percentages (3% − 2% = 1 percentage point); percent measures the relative change ((3 − 2) ÷ 2 = 50%).\n- Ratios are simplified exactly: decimal terms are first multiplied by a power of 10 to make whole numbers, then divided by their greatest common divisor.\n- Splitting an amount by a ratio uses the largest-remainder method at the chosen decimal places, so the parts always add up to exactly the amount.\n- Aspect ratios: width:height simplified by the GCD, the decimal width ÷ height, and the nearest well-known ratio within 1%. Resizing keeps the ratio: new height = new width × height ÷ width.",
        "tags": [
          "calculators"
        ],
        "x-mcp-tool": "percentage_ratio_calculator",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PercentageRatioCalculatorInput"
              },
              "examples": {
                "example1": {
                  "summary": "What is 15% of 80?",
                  "value": {
                    "mode": "percent-of",
                    "x": 15,
                    "y": 80
                  }
                },
                "example2": {
                  "summary": "Price rose from 40 to 50: percentage change",
                  "value": {
                    "mode": "change",
                    "x": 40,
                    "y": 50
                  }
                },
                "example3": {
                  "summary": "Price after 19% VAT is 119: price before tax",
                  "value": {
                    "mode": "reverse",
                    "x": 19,
                    "y": 119
                  }
                },
                "example4": {
                  "summary": "Interest rate from 2% to 3%: points vs percent",
                  "value": {
                    "mode": "points",
                    "x": 2,
                    "y": 3
                  }
                },
                "example5": {
                  "summary": "Split 1,000 by 2:3:5",
                  "value": {
                    "mode": "ratio-split",
                    "ratio": "2:3:5",
                    "amount": 1000
                  }
                },
                "example6": {
                  "summary": "1920 × 1080 aspect ratio, resize to 1280 wide",
                  "value": {
                    "mode": "aspect",
                    "x": 1920,
                    "y": 1080,
                    "newWidth": 1280
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Calculation result envelope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PercentageRatioCalculatorResult"
                }
              }
            }
          },
          "400": {
            "description": "Malformed JSON body.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "Invalid input (see errors[]).",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded (see Retry-After).",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/tools/random-generator/calculate": {
      "post": {
        "operationId": "calculateRandomGenerator",
        "summary": "Random numbers, dice, coins, list picks, shuffles, fair teams, dates, strings and UUIDs.",
        "description": "Generates random whole or decimal numbers (optionally unique), dice rolls such as 3d6+2, coin flips, picks from a list, shuffles, balanced teams (by optional skill), random dates, strings and UUID v4/v7. Uses cryptographic randomness by default; with a seed the output is reproducible so others can verify a draw.\n\nMethodology:\n- Without a seed, bytes come from the server's cryptographically secure generator (Node.js crypto.randomBytes); nothing is stored. With a seed, bytes come from SHA-256 in counter mode over the seed, so anyone can reproduce the draw with the same seed and settings.\n- Whole numbers in a range use rejection sampling (no modulo bias). Decimals use 53 random bits scaled into [min, max) and rounded to the chosen places. Unique numbers use a partial Fisher–Yates shuffle or Floyd's algorithm for huge ranges.\n- Shuffles and picks use Fisher–Yates. Teams: without skills, a random round-robin; with skills, a snake draft over a random order followed by swaps that shrink the gap between the strongest and weakest team.\n- UUID v4 has 122 random bits; UUID v7 has a 48-bit Unix millisecond timestamp and 74 random bits (RFC 9562).\n\nSeeded output is predictable by design. For passwords or keys, leave the seed empty (or use a dedicated password generator on your own device).",
        "tags": [
          "calculators"
        ],
        "x-mcp-tool": "random_generator",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RandomGeneratorInput"
              },
              "examples": {
                "example1": {
                  "summary": "Lottery 6 of 49 (seeded)",
                  "value": {
                    "mode": "integers",
                    "count": 6,
                    "min": 1,
                    "max": 49,
                    "unique": true,
                    "seed": "draw-2026-10-18"
                  }
                },
                "example2": {
                  "summary": "Roll 4d6+2 five times (seeded)",
                  "value": {
                    "mode": "dice",
                    "count": 5,
                    "dice": "4d6+2",
                    "seed": "demo"
                  }
                },
                "example3": {
                  "summary": "Balanced teams by skill (seeded)",
                  "value": {
                    "mode": "teams",
                    "teams": 2,
                    "items": [
                      "Ana, 8",
                      "Ben, 5",
                      "Cleo, 9",
                      "Dev, 4",
                      "Eli, 6",
                      "Fay, 7"
                    ],
                    "seed": "friday"
                  }
                },
                "example4": {
                  "summary": "Three UUID v4 (seeded)",
                  "value": {
                    "mode": "uuid4",
                    "count": 3,
                    "seed": "example"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Calculation result envelope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RandomGeneratorResult"
                }
              }
            }
          },
          "400": {
            "description": "Malformed JSON body.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "Invalid input (see errors[]).",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded (see Retry-After).",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/tools/running-pace-calculator/calculate": {
      "post": {
        "operationId": "calculateRunningPaceCalculator",
        "summary": "Pace, time or distance in km or miles, splits, race predictions, treadmill speed, laps.",
        "description": "Enter two of distance, time and pace to get the third, in kilometres or miles. Get an even, negative or positive split table, race time predictions with the Riegel formula, the treadmill speed in km/h and mph, and track lap times.\n\nMethodology:\n- Pace = time ÷ distance; time = pace × distance; distance = time ÷ pace. 1 mile = 1609.344 m exactly; a half marathon is 21.0975 km and a marathon 42.195 km.\n- Speed (treadmill) = 3600 ÷ pace in seconds per km (km/h) or per mile (mph).\n- Splits: for an even pace every split takes pace × split length. For a negative (positive) split the pace changes linearly along the course by the chosen percentage from start to finish, so the split times still add up to the target time.\n- Race predictions use Riegel's formula T2 = T1 × (D2 ÷ D1)^1.06, which he described for efforts of roughly 3.5 to 230 minutes. Predictions far from the input distance, and marathon times for runners without long-run training, are usually too optimistic.\n- Track laps: lap time = pace × lap length. Lane 1 of a standard track is 400 m; outer lanes are longer.",
        "tags": [
          "calculators"
        ],
        "x-mcp-tool": "running_pace_calculator",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RunningPaceCalculatorInput"
              },
              "examples": {
                "example1": {
                  "summary": "10K in 50:00: pace and splits",
                  "value": {
                    "solveFor": "pace",
                    "preset": "10k",
                    "time": "50:00",
                    "splitUnit": "km",
                    "splitStrategy": "even"
                  }
                },
                "example2": {
                  "summary": "Marathon at 5:40/km with a 3% negative split",
                  "value": {
                    "solveFor": "time",
                    "preset": "marathon",
                    "pace": "5:40",
                    "paceUnit": "km",
                    "splitUnit": "km",
                    "splitStrategy": "negative",
                    "splitPercent": 3
                  }
                },
                "example3": {
                  "summary": "How far in 1 hour at 8:30/mile?",
                  "value": {
                    "solveFor": "distance",
                    "time": "1:00:00",
                    "pace": "8:30",
                    "paceUnit": "mi",
                    "splitUnit": "mi"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Calculation result envelope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RunningPaceCalculatorResult"
                }
              }
            }
          },
          "400": {
            "description": "Malformed JSON body.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "Invalid input (see errors[]).",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded (see Retry-After).",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/tools/sports-travel-fatigue-estimator/calculate": {
      "post": {
        "operationId": "calculateSportsTravelFatigueEstimator",
        "summary": "Travel distance, time-zone jumps, rest and back-to-backs per game as a fatigue score.",
        "description": "Enter a team's schedule (date, city or coordinates). For every game the estimator computes the great-circle travel distance from the previous venue, the time-zone shift and its direction, rest days, back-to-backs and schedule density, and combines them into a 0–100 fatigue score with openly documented weights. It is a heuristic for comparing schedules, not a physiological measurement.\n\nMethodology:\n- Travel distance: great-circle (haversine) distance between the previous venue (or the starting city) and this venue, with a mean Earth radius of 6371 km. Real road or flight routes are longer.\n- Time-zone shift = UTC offset at the venue − UTC offset at the previous venue, on the game date (daylight saving time included via the IANA time-zone database). Positive = eastward (clocks ahead).\n- Fatigue score (0–100, heuristic): travel 1 point per 100 km (max 30); time zones: hours of shift not yet recovered × 5 (eastward) or × 3.5 (westward), max 25, assuming recovery of 1 h per full day between games eastward and 1.5 h westward; rest: same-day game 30, back-to-back (next day) 20, one rest day 8, two rest days 3; density: 5 points for each game beyond 3 in the 7 days ending on the game day (max 15).\n- Eastward travel is weighted more because research on Major League Baseball (Song, Severini & Allada, PNAS 2017) found jet-lag effects mainly after eastward travel. The exact weights are editorial choices to make schedules comparable, not fitted to data.\n- Bands: below 25 low, 25–49 moderate, 50–74 high, 75 and above very high.\n\nA transparent heuristic for comparing schedules. It does not measure an athlete's actual fatigue, sleep or readiness.",
        "tags": [
          "calculators"
        ],
        "x-mcp-tool": "sports_travel_fatigue_estimator",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SportsTravelFatigueEstimatorInput"
              },
              "examples": {
                "example1": {
                  "summary": "East-coast basketball team on a western road trip",
                  "value": {
                    "homeBase": "Boston",
                    "games": [
                      {
                        "date": "2026-11-02",
                        "time": "19:30",
                        "location": "Boston",
                        "label": "Home"
                      },
                      {
                        "date": "2026-11-04",
                        "time": "19:00",
                        "location": "Denver"
                      },
                      {
                        "date": "2026-11-05",
                        "time": "19:00",
                        "location": "Phoenix"
                      },
                      {
                        "date": "2026-11-07",
                        "time": "19:30",
                        "location": "Los Angeles"
                      },
                      {
                        "date": "2026-11-09",
                        "time": "19:00",
                        "location": "Portland"
                      },
                      {
                        "date": "2026-11-10",
                        "time": "19:00",
                        "location": "Seattle"
                      },
                      {
                        "date": "2026-11-12",
                        "time": "19:30",
                        "location": "Boston",
                        "label": "Home"
                      }
                    ]
                  }
                },
                "example2": {
                  "summary": "European club: league and Champions League week",
                  "value": {
                    "homeBase": "Munich",
                    "games": [
                      {
                        "date": "2026-09-19",
                        "time": "15:30",
                        "location": "Munich"
                      },
                      {
                        "date": "2026-09-23",
                        "time": "21:00",
                        "location": "Lisbon"
                      },
                      {
                        "date": "2026-09-26",
                        "time": "18:30",
                        "location": "Hamburg"
                      },
                      {
                        "date": "2026-09-30",
                        "time": "21:00",
                        "location": "Istanbul"
                      },
                      {
                        "date": "2026-10-03",
                        "time": "15:30",
                        "location": "Munich"
                      }
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Calculation result envelope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SportsTravelFatigueEstimatorResult"
                }
              }
            }
          },
          "400": {
            "description": "Malformed JSON body.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "Invalid input (see errors[]).",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded (see Retry-After).",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/tools/tip-bill-split-calculator/calculate": {
      "post": {
        "operationId": "calculateTipBillSplitCalculator",
        "summary": "Tip, tax and who pays what: split a bill equally, by shares or by items, rounded up.",
        "description": "Enter the bill, the tax rule (included as in most of Europe, or added at the till as in the US) and a tip as a percentage or an amount, before or after tax. Split equally, by shares or by what each person ordered, round each share up to 0.50 or 1, and get an exact who-pays-what table that adds up to the cent.\n\nMethodology:\n- Tax included (VAT): subtotal before tax = bill ÷ (1 + rate); tax = bill − subtotal. Tax added (sales tax): tax = bill × rate, rounded to the cent; bill total = bill + tax.\n- Tip = tip % × tip base, where the base is the subtotal before tax or the total with tax (your choice), rounded to the cent; or a fixed tip amount.\n- Weights: equal = 1 per person; shares = the share you enter; items = each person's ordered amount plus an equal part of anything not assigned to anyone.\n- Bill, tax and tip are each split by weight with the largest-remainder method in whole cents, so the shares add up exactly to the totals.\n- Rounding is applied per person to their exact total; the difference between what is collected and the exact total goes to the tip and is shown separately.",
        "tags": [
          "calculators"
        ],
        "x-mcp-tool": "tip_bill_split_calculator",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TipBillSplitCalculatorInput"
              },
              "examples": {
                "example1": {
                  "summary": "Dinner for 4 in Germany, 84.50 incl. VAT, 10% tip, round up to 1",
                  "value": {
                    "symbol": "€",
                    "billAmount": 84.5,
                    "taxMode": "included",
                    "taxPercent": 19,
                    "tipMode": "percent",
                    "tipPercent": 10,
                    "tipBase": "post-tax",
                    "splitMode": "equal",
                    "people": 4,
                    "rounding": "up-1"
                  }
                },
                "example2": {
                  "summary": "US restaurant, 120 + 8.875% sales tax, 20% tip on pre-tax, by items",
                  "value": {
                    "symbol": "$",
                    "billAmount": 120,
                    "taxMode": "added",
                    "taxPercent": 8.875,
                    "tipMode": "percent",
                    "tipPercent": 20,
                    "tipBase": "pre-tax",
                    "splitMode": "items",
                    "persons": [
                      {
                        "name": "Ana",
                        "items": 34
                      },
                      {
                        "name": "Ben",
                        "items": 46
                      },
                      {
                        "name": "Chloe",
                        "items": 28
                      }
                    ],
                    "rounding": "none"
                  }
                },
                "example3": {
                  "summary": "Couple + 2 singles by shares, fixed tip 15",
                  "value": {
                    "symbol": "£",
                    "billAmount": 150,
                    "taxMode": "none",
                    "taxPercent": 0,
                    "tipMode": "amount",
                    "tipAmount": 15,
                    "splitMode": "shares",
                    "persons": [
                      {
                        "name": "Sam & Alex",
                        "shares": 2
                      },
                      {
                        "name": "Jo",
                        "shares": 1
                      },
                      {
                        "name": "Kim",
                        "shares": 1
                      }
                    ],
                    "rounding": "up-0.5"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Calculation result envelope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TipBillSplitCalculatorResult"
                }
              }
            }
          },
          "400": {
            "description": "Malformed JSON body.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "Invalid input (see errors[]).",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded (see Retry-After).",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/workflows": {
      "get": {
        "operationId": "listWorkflows",
        "summary": "Multi-tool workflows",
        "responses": {
          "200": {
            "description": "Workflow list.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "Problem": {
        "type": "object",
        "description": "RFC 9457 problem details. 422 responses include per-field `errors`.",
        "required": [
          "type",
          "title",
          "status",
          "detail"
        ],
        "properties": {
          "type": {
            "type": "string"
          },
          "title": {
            "type": "string"
          },
          "status": {
            "type": "integer"
          },
          "detail": {
            "type": "string"
          },
          "code": {
            "type": "string"
          },
          "errors": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "field",
                "message"
              ],
              "properties": {
                "field": {
                  "type": "string"
                },
                "message": {
                  "type": "string"
                }
              }
            }
          }
        }
      },
      "LoanAmortizationCalculatorInput": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "principal": {
            "title": "Loan amount",
            "type": "number",
            "minimum": 1,
            "maximum": 10000000000,
            "default": 200000
          },
          "currency": {
            "title": "Currency",
            "description": "Shown in results only; no conversion.",
            "type": "string",
            "maxLength": 8,
            "default": "EUR"
          },
          "ratePercent": {
            "title": "Nominal interest rate",
            "description": "Borrowing rate (Sollzins). The rate per period is this ÷ payments per year.",
            "type": "number",
            "minimum": 0,
            "maximum": 100,
            "default": 3.5,
            "x-unit": "% per year"
          },
          "loanType": {
            "title": "Repayment type",
            "type": "string",
            "enum": [
              "annuity",
              "linear",
              "interest-only"
            ],
            "default": "annuity"
          },
          "termYears": {
            "title": "Term",
            "type": "number",
            "minimum": 0.25,
            "maximum": 100,
            "default": 25,
            "x-unit": "years"
          },
          "initialRepaymentPercent": {
            "title": "Initial repayment rate (annuity only)",
            "description": "Optional, common in Germany (anfängliche Tilgung): payment per year = loan × (interest rate + this rate). The term then follows from the payment and the Term field is ignored.",
            "type": "number",
            "minimum": 0.01,
            "maximum": 100,
            "x-unit": "% per year"
          },
          "frequency": {
            "title": "Payments",
            "type": "string",
            "enum": [
              "monthly",
              "quarterly",
              "annually"
            ],
            "default": "monthly"
          },
          "startDate": {
            "title": "Payout date",
            "description": "Optional. The first payment is due one period later; dates are added to the schedule.",
            "type": "string",
            "format": "date"
          },
          "fixedRateYears": {
            "title": "Fixed-rate period",
            "description": "Optional (Zinsbindung): shows the balance still owed when the fixed rate ends.",
            "type": "number",
            "minimum": 0.25,
            "maximum": 100,
            "x-unit": "years"
          },
          "upfrontFees": {
            "title": "Up-front fees",
            "description": "Processing or broker fees paid at payout. They raise the APR, not the nominal rate.",
            "type": "number",
            "minimum": 0,
            "default": 0
          },
          "extraPayments": {
            "title": "One-off extra payments",
            "description": "Paid together with the regular payment number you enter (e.g. 12 = end of the first year for monthly payments).",
            "type": "array",
            "maxItems": 30,
            "items": {
              "type": "object",
              "additionalProperties": false,
              "properties": {
                "period": {
                  "title": "Payment number",
                  "type": "integer",
                  "minimum": 1,
                  "maximum": 1200
                },
                "amount": {
                  "title": "Extra amount",
                  "type": "number",
                  "minimum": 0
                }
              },
              "required": [
                "period",
                "amount"
              ]
            }
          },
          "recurringExtra": {
            "title": "Recurring extra payment",
            "description": "Optional, e.g. an annual special repayment (Sondertilgung).",
            "type": "number",
            "minimum": 0,
            "default": 0
          },
          "recurringEvery": {
            "title": "Recurring extra: every",
            "type": "integer",
            "minimum": 1,
            "maximum": 120,
            "default": 12,
            "x-unit": "payments"
          },
          "recurringFrom": {
            "title": "Recurring extra: first at payment number",
            "type": "integer",
            "minimum": 1,
            "maximum": 1200,
            "default": 12
          },
          "recurringUntil": {
            "title": "Recurring extra: last at payment number",
            "description": "Optional; empty = until the loan is repaid.",
            "type": "integer",
            "minimum": 1,
            "maximum": 1200
          }
        },
        "required": [
          "principal"
        ]
      },
      "LoanAmortizationCalculatorResult": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "schemaVersion",
          "tool",
          "computedAt",
          "inputs",
          "summary",
          "result",
          "units",
          "assumptions",
          "sources",
          "warnings",
          "presentation",
          "links"
        ],
        "properties": {
          "schemaVersion": {
            "type": "string",
            "const": "1.0"
          },
          "tool": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "id",
              "name",
              "version"
            ],
            "properties": {
              "id": {
                "type": "string"
              },
              "name": {
                "type": "string"
              },
              "version": {
                "type": "string"
              }
            }
          },
          "computedAt": {
            "type": "string",
            "format": "date-time"
          },
          "inputs": {
            "type": "object",
            "description": "Normalised inputs actually used, including applied defaults."
          },
          "summary": {
            "type": "string"
          },
          "result": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "currency",
              "loanType",
              "periodsPerYear",
              "principal",
              "nominalRatePercent",
              "periodicRatePercent",
              "regularPayment",
              "numberOfPayments",
              "termYears",
              "totalInterest",
              "totalExtra",
              "totalPaid",
              "payoffDate",
              "effectiveRatePercent",
              "aprPercent",
              "fixedRate",
              "extrasSavings",
              "schedule"
            ],
            "properties": {
              "currency": {
                "type": "string"
              },
              "loanType": {
                "type": "string",
                "enum": [
                  "annuity",
                  "linear",
                  "interest-only"
                ]
              },
              "periodsPerYear": {
                "type": "integer"
              },
              "principal": {
                "type": "number"
              },
              "nominalRatePercent": {
                "type": "number"
              },
              "periodicRatePercent": {
                "type": "number"
              },
              "regularPayment": {
                "type": "number",
                "description": "Annuity: the constant payment; linear and interest-only: the first payment."
              },
              "numberOfPayments": {
                "type": "integer"
              },
              "termYears": {
                "type": "number"
              },
              "totalInterest": {
                "type": "number"
              },
              "totalExtra": {
                "type": "number"
              },
              "totalPaid": {
                "type": "number"
              },
              "payoffDate": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "effectiveRatePercent": {
                "type": "number",
                "description": "(1 + nominal/m)^m − 1, without fees."
              },
              "aprPercent": {
                "type": [
                  "number",
                  "null"
                ],
                "description": "Annual rate equating the net payout (loan − fees) with all payments (PAngV/CCD equation, standard months)."
              },
              "fixedRate": {
                "type": [
                  "object",
                  "null"
                ],
                "additionalProperties": false,
                "required": [
                  "years",
                  "payments",
                  "balanceAtEnd",
                  "interestPaid",
                  "principalRepaid",
                  "endDate"
                ],
                "properties": {
                  "years": {
                    "type": "number"
                  },
                  "payments": {
                    "type": "integer"
                  },
                  "balanceAtEnd": {
                    "type": "number"
                  },
                  "interestPaid": {
                    "type": "number"
                  },
                  "principalRepaid": {
                    "type": "number"
                  },
                  "endDate": {
                    "type": [
                      "string",
                      "null"
                    ]
                  }
                }
              },
              "extrasSavings": {
                "type": [
                  "object",
                  "null"
                ],
                "additionalProperties": false,
                "required": [
                  "interestSaved",
                  "paymentsSaved"
                ],
                "properties": {
                  "interestSaved": {
                    "type": "number"
                  },
                  "paymentsSaved": {
                    "type": "integer"
                  }
                }
              },
              "schedule": {
                "type": "array",
                "items": {
                  "type": "object",
                  "additionalProperties": false,
                  "required": [
                    "period",
                    "date",
                    "payment",
                    "interest",
                    "principal",
                    "extra",
                    "balance"
                  ],
                  "properties": {
                    "period": {
                      "type": "integer"
                    },
                    "date": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "payment": {
                      "type": "number"
                    },
                    "interest": {
                      "type": "number"
                    },
                    "principal": {
                      "type": "number"
                    },
                    "extra": {
                      "type": "number"
                    },
                    "balance": {
                      "type": "number"
                    }
                  }
                }
              }
            }
          },
          "units": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            }
          },
          "assumptions": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "sources": {
            "type": "array",
            "items": {
              "type": "object",
              "additionalProperties": false,
              "required": [
                "title"
              ],
              "properties": {
                "title": {
                  "type": "string"
                },
                "url": {
                  "type": "string"
                },
                "publisher": {
                  "type": "string"
                },
                "retrievedAt": {
                  "type": "string"
                },
                "note": {
                  "type": "string"
                }
              }
            }
          },
          "warnings": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "presentation": {
            "type": "array",
            "items": {
              "type": "object"
            },
            "description": "Display blocks used by the human page (same data)."
          },
          "links": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "human",
              "api",
              "openapi",
              "methodology"
            ],
            "properties": {
              "human": {
                "type": "string",
                "format": "uri"
              },
              "api": {
                "type": "string",
                "format": "uri"
              },
              "openapi": {
                "type": "string",
                "format": "uri"
              },
              "methodology": {
                "type": "string",
                "format": "uri"
              }
            }
          }
        }
      },
      "OneRepMaxCalculatorInput": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "weight": {
            "title": "Weight lifted",
            "type": "number",
            "minimum": 0.5,
            "maximum": 2000,
            "default": 100
          },
          "reps": {
            "title": "Repetitions",
            "description": "Reps completed in the set. Estimates are most reliable up to about 10 reps.",
            "type": "integer",
            "minimum": 1,
            "maximum": 30,
            "default": 5
          },
          "unit": {
            "title": "Unit",
            "type": "string",
            "enum": [
              "kg",
              "lb"
            ],
            "default": "kg"
          },
          "rpe": {
            "title": "RPE of the set",
            "description": "Optional (6–10). 10 = to failure; 8 = two more reps were possible. Reps in reserve are added before estimating.",
            "type": "number",
            "minimum": 6,
            "maximum": 10
          },
          "targetWeight": {
            "title": "Weight to load",
            "description": "Optional total bar weight for the plate loader.",
            "type": "number",
            "minimum": 0,
            "maximum": 2000
          },
          "targetPercent": {
            "title": "Or: percentage of 1RM to load",
            "description": "Used when no weight to load is given; default 100% of the average estimate.",
            "type": "number",
            "minimum": 1,
            "maximum": 110,
            "x-unit": "%"
          },
          "barWeight": {
            "title": "Bar weight",
            "description": "Default 20 kg or 45 lb.",
            "type": "number",
            "minimum": 0,
            "maximum": 100
          },
          "collarWeight": {
            "title": "Collars (both together)",
            "type": "number",
            "minimum": 0,
            "maximum": 20,
            "default": 0
          },
          "plates": {
            "title": "Plates available",
            "description": "Plate weight and how many you have in total (both sides). Empty = a standard set (kg: 25–1.25, lb: 45–2.5).",
            "type": "array",
            "maxItems": 15,
            "items": {
              "type": "object",
              "additionalProperties": false,
              "properties": {
                "weight": {
                  "title": "Plate weight",
                  "type": "number",
                  "minimum": 0.05,
                  "maximum": 100
                },
                "count": {
                  "title": "Count",
                  "type": "integer",
                  "minimum": 0,
                  "maximum": 100
                }
              },
              "required": [
                "weight",
                "count"
              ]
            }
          }
        },
        "required": [
          "weight",
          "reps"
        ]
      },
      "OneRepMaxCalculatorResult": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "schemaVersion",
          "tool",
          "computedAt",
          "inputs",
          "summary",
          "result",
          "units",
          "assumptions",
          "sources",
          "warnings",
          "presentation",
          "links"
        ],
        "properties": {
          "schemaVersion": {
            "type": "string",
            "const": "1.0"
          },
          "tool": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "id",
              "name",
              "version"
            ],
            "properties": {
              "id": {
                "type": "string"
              },
              "name": {
                "type": "string"
              },
              "version": {
                "type": "string"
              }
            }
          },
          "computedAt": {
            "type": "string",
            "format": "date-time"
          },
          "inputs": {
            "type": "object",
            "description": "Normalised inputs actually used, including applied defaults."
          },
          "summary": {
            "type": "string"
          },
          "result": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "unit",
              "weight",
              "reps",
              "effectiveReps",
              "estimates",
              "average",
              "percentTable",
              "rpeTable",
              "loader"
            ],
            "properties": {
              "unit": {
                "type": "string",
                "enum": [
                  "kg",
                  "lb"
                ]
              },
              "weight": {
                "type": "number"
              },
              "reps": {
                "type": "integer"
              },
              "effectiveReps": {
                "type": "number",
                "description": "Reps + reps in reserve (10 − RPE)."
              },
              "estimates": {
                "type": "array",
                "items": {
                  "type": "object",
                  "additionalProperties": false,
                  "required": [
                    "id",
                    "name",
                    "formula",
                    "oneRepMax"
                  ],
                  "properties": {
                    "id": {
                      "type": "string"
                    },
                    "name": {
                      "type": "string"
                    },
                    "formula": {
                      "type": "string"
                    },
                    "oneRepMax": {
                      "type": "number"
                    }
                  }
                }
              },
              "average": {
                "type": "number"
              },
              "percentTable": {
                "type": "array",
                "items": {
                  "type": "object",
                  "additionalProperties": false,
                  "required": [
                    "percent",
                    "weight",
                    "approxReps"
                  ],
                  "properties": {
                    "percent": {
                      "type": "number"
                    },
                    "weight": {
                      "type": "number"
                    },
                    "approxReps": {
                      "type": [
                        "number",
                        "null"
                      ]
                    }
                  }
                }
              },
              "rpeTable": {
                "type": "array",
                "items": {
                  "type": "object",
                  "additionalProperties": false,
                  "required": [
                    "reps",
                    "rpe",
                    "percent",
                    "weight"
                  ],
                  "properties": {
                    "reps": {
                      "type": "integer"
                    },
                    "rpe": {
                      "type": "number"
                    },
                    "percent": {
                      "type": "number"
                    },
                    "weight": {
                      "type": "number"
                    }
                  }
                }
              },
              "loader": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "target",
                  "bar",
                  "collars",
                  "exact",
                  "perSide",
                  "loaded",
                  "method",
                  "nearestBelow",
                  "nearestAbove"
                ],
                "properties": {
                  "target": {
                    "type": "number"
                  },
                  "bar": {
                    "type": "number"
                  },
                  "collars": {
                    "type": "number"
                  },
                  "exact": {
                    "type": "boolean"
                  },
                  "perSide": {
                    "type": "array",
                    "items": {
                      "type": "number"
                    }
                  },
                  "loaded": {
                    "type": [
                      "number",
                      "null"
                    ]
                  },
                  "method": {
                    "type": [
                      "string",
                      "null"
                    ]
                  },
                  "nearestBelow": {
                    "type": [
                      "object",
                      "null"
                    ],
                    "additionalProperties": false,
                    "properties": {
                      "total": {
                        "type": "number"
                      },
                      "perSide": {
                        "type": "array",
                        "items": {
                          "type": "number"
                        }
                      }
                    }
                  },
                  "nearestAbove": {
                    "type": [
                      "object",
                      "null"
                    ],
                    "additionalProperties": false,
                    "properties": {
                      "total": {
                        "type": "number"
                      },
                      "perSide": {
                        "type": "array",
                        "items": {
                          "type": "number"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "units": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            }
          },
          "assumptions": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "sources": {
            "type": "array",
            "items": {
              "type": "object",
              "additionalProperties": false,
              "required": [
                "title"
              ],
              "properties": {
                "title": {
                  "type": "string"
                },
                "url": {
                  "type": "string"
                },
                "publisher": {
                  "type": "string"
                },
                "retrievedAt": {
                  "type": "string"
                },
                "note": {
                  "type": "string"
                }
              }
            }
          },
          "warnings": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "presentation": {
            "type": "array",
            "items": {
              "type": "object"
            },
            "description": "Display blocks used by the human page (same data)."
          },
          "links": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "human",
              "api",
              "openapi",
              "methodology"
            ],
            "properties": {
              "human": {
                "type": "string",
                "format": "uri"
              },
              "api": {
                "type": "string",
                "format": "uri"
              },
              "openapi": {
                "type": "string",
                "format": "uri"
              },
              "methodology": {
                "type": "string",
                "format": "uri"
              }
            }
          }
        }
      },
      "PercentageRatioCalculatorInput": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "mode": {
            "title": "Question",
            "type": "string",
            "enum": [
              "percent-of",
              "what-percent",
              "change",
              "increase",
              "decrease",
              "reverse",
              "points",
              "ratio-simplify",
              "ratio-scale",
              "ratio-split",
              "aspect"
            ],
            "default": "percent-of"
          },
          "x": {
            "title": "Value X",
            "description": "Percentage modes: the percentage or the start value (see the question). Aspect ratio: width.",
            "type": "number"
          },
          "y": {
            "title": "Value Y",
            "description": "Percentage modes: the base, end or final value. Aspect ratio: height.",
            "type": "number"
          },
          "ratio": {
            "title": "Ratio",
            "description": "For the ratio modes: 2 to 10 positive terms separated by \":\" (e.g. 16:9, 2:3:5, 1.5:2).",
            "type": "string",
            "maxLength": 120
          },
          "amount": {
            "title": "Amount",
            "description": "Split: the total to share out. Scale: the value the chosen term should become.",
            "type": "number"
          },
          "scaleTerm": {
            "title": "Term to scale",
            "description": "Scale mode: which ratio term (1 = first) should equal the amount.",
            "type": "integer",
            "minimum": 1,
            "maximum": 10,
            "default": 1
          },
          "newWidth": {
            "title": "New width",
            "description": "Aspect ratio: optional; the matching height is calculated.",
            "type": "number",
            "minimum": 0
          },
          "newHeight": {
            "title": "New height",
            "description": "Aspect ratio: optional; the matching width is calculated.",
            "type": "number",
            "minimum": 0
          },
          "decimals": {
            "title": "Decimal places",
            "description": "Rounding of the displayed answer and of split amounts (2 = cents).",
            "type": "integer",
            "minimum": 0,
            "maximum": 10,
            "default": 2
          }
        },
        "required": []
      },
      "PercentageRatioCalculatorResult": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "schemaVersion",
          "tool",
          "computedAt",
          "inputs",
          "summary",
          "result",
          "units",
          "assumptions",
          "sources",
          "warnings",
          "presentation",
          "links"
        ],
        "properties": {
          "schemaVersion": {
            "type": "string",
            "const": "1.0"
          },
          "tool": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "id",
              "name",
              "version"
            ],
            "properties": {
              "id": {
                "type": "string"
              },
              "name": {
                "type": "string"
              },
              "version": {
                "type": "string"
              }
            }
          },
          "computedAt": {
            "type": "string",
            "format": "date-time"
          },
          "inputs": {
            "type": "object",
            "description": "Normalised inputs actually used, including applied defaults."
          },
          "summary": {
            "type": "string"
          },
          "result": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "mode",
              "question",
              "answer",
              "answerText",
              "formula",
              "steps",
              "ratio"
            ],
            "properties": {
              "mode": {
                "type": "string",
                "enum": [
                  "percent-of",
                  "what-percent",
                  "change",
                  "increase",
                  "decrease",
                  "reverse",
                  "points",
                  "ratio-simplify",
                  "ratio-scale",
                  "ratio-split",
                  "aspect"
                ]
              },
              "question": {
                "type": "string"
              },
              "answer": {
                "type": [
                  "number",
                  "null"
                ],
                "description": "Main numeric answer, rounded to the chosen decimals (null for ratio-only answers)."
              },
              "answerText": {
                "type": "string"
              },
              "formula": {
                "type": "string"
              },
              "steps": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "ratio": {
                "type": [
                  "object",
                  "null"
                ],
                "additionalProperties": false,
                "properties": {
                  "terms": {
                    "type": "array",
                    "items": {
                      "type": "number"
                    }
                  },
                  "simplified": {
                    "type": "array",
                    "items": {
                      "type": "number"
                    }
                  },
                  "unitForm": {
                    "type": "array",
                    "items": {
                      "type": "number"
                    },
                    "description": "Ratio divided by its first term (1 : n)."
                  },
                  "parts": {
                    "type": "array",
                    "items": {
                      "type": "number"
                    },
                    "description": "Split amounts (sum exactly to the amount) or scaled terms."
                  },
                  "decimal": {
                    "type": [
                      "number",
                      "null"
                    ],
                    "description": "Aspect ratio as a decimal (width ÷ height)."
                  },
                  "nearestNamed": {
                    "type": [
                      "string",
                      "null"
                    ]
                  },
                  "newWidth": {
                    "type": [
                      "number",
                      "null"
                    ]
                  },
                  "newHeight": {
                    "type": [
                      "number",
                      "null"
                    ]
                  }
                }
              }
            }
          },
          "units": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            }
          },
          "assumptions": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "sources": {
            "type": "array",
            "items": {
              "type": "object",
              "additionalProperties": false,
              "required": [
                "title"
              ],
              "properties": {
                "title": {
                  "type": "string"
                },
                "url": {
                  "type": "string"
                },
                "publisher": {
                  "type": "string"
                },
                "retrievedAt": {
                  "type": "string"
                },
                "note": {
                  "type": "string"
                }
              }
            }
          },
          "warnings": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "presentation": {
            "type": "array",
            "items": {
              "type": "object"
            },
            "description": "Display blocks used by the human page (same data)."
          },
          "links": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "human",
              "api",
              "openapi",
              "methodology"
            ],
            "properties": {
              "human": {
                "type": "string",
                "format": "uri"
              },
              "api": {
                "type": "string",
                "format": "uri"
              },
              "openapi": {
                "type": "string",
                "format": "uri"
              },
              "methodology": {
                "type": "string",
                "format": "uri"
              }
            }
          }
        }
      },
      "RandomGeneratorInput": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "mode": {
            "title": "Generate",
            "type": "string",
            "enum": [
              "integers",
              "floats",
              "dice",
              "coins",
              "pick",
              "shuffle",
              "teams",
              "dates",
              "strings",
              "uuid4",
              "uuid7"
            ],
            "default": "integers"
          },
          "count": {
            "title": "How many",
            "description": "Numbers, rolls, flips, picks, dates, strings or UUIDs.",
            "type": "integer",
            "minimum": 1,
            "maximum": 1000,
            "default": 10
          },
          "min": {
            "title": "Minimum",
            "type": "number",
            "minimum": -9007199254740991,
            "maximum": 9007199254740991,
            "default": 1
          },
          "max": {
            "title": "Maximum",
            "type": "number",
            "minimum": -9007199254740991,
            "maximum": 9007199254740991,
            "default": 100
          },
          "unique": {
            "title": "No repeats (numbers and picks)",
            "type": "boolean",
            "default": false
          },
          "decimals": {
            "title": "Decimal places",
            "type": "integer",
            "minimum": 0,
            "maximum": 15,
            "default": 2
          },
          "dice": {
            "title": "Dice notation",
            "description": "NdS±M, e.g. 2d20, 4d6+2, d100.",
            "type": "string",
            "maxLength": 40,
            "default": "3d6"
          },
          "items": {
            "title": "List (one per line; \"name, skill\" for teams)",
            "type": "array",
            "items": {
              "type": "string"
            },
            "maxItems": 1000
          },
          "teams": {
            "title": "Number of teams",
            "type": "integer",
            "minimum": 2,
            "maximum": 50,
            "default": 2
          },
          "dateFrom": {
            "title": "Earliest date",
            "type": "string",
            "format": "date",
            "default": "2026-01-01"
          },
          "dateTo": {
            "title": "Latest date",
            "type": "string",
            "format": "date",
            "default": "2026-12-31"
          },
          "length": {
            "title": "String length",
            "type": "integer",
            "minimum": 1,
            "maximum": 256,
            "default": 16
          },
          "charset": {
            "title": "Characters",
            "type": "string",
            "enum": [
              "alnum",
              "unambiguous",
              "lower",
              "upper",
              "digits",
              "hex",
              "symbols"
            ],
            "default": "alnum"
          },
          "seed": {
            "title": "Seed (optional, for reproducible results)",
            "description": "Empty = cryptographic randomness, different every time. With a seed, the same settings always give the same result — never use seeded output for passwords or keys.",
            "type": "string",
            "maxLength": 200
          },
          "uuidTime": {
            "title": "UUID v7 timestamp (optional)",
            "description": "Default: now (UTC). Set it together with a seed for reproducible v7 UUIDs. Local date-time YYYY-MM-DDTHH:mm",
            "type": "string"
          }
        },
        "required": []
      },
      "RandomGeneratorResult": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "schemaVersion",
          "tool",
          "computedAt",
          "inputs",
          "summary",
          "result",
          "units",
          "assumptions",
          "sources",
          "warnings",
          "presentation",
          "links"
        ],
        "properties": {
          "schemaVersion": {
            "type": "string",
            "const": "1.0"
          },
          "tool": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "id",
              "name",
              "version"
            ],
            "properties": {
              "id": {
                "type": "string"
              },
              "name": {
                "type": "string"
              },
              "version": {
                "type": "string"
              }
            }
          },
          "computedAt": {
            "type": "string",
            "format": "date-time"
          },
          "inputs": {
            "type": "object",
            "description": "Normalised inputs actually used, including applied defaults."
          },
          "summary": {
            "type": "string"
          },
          "result": {
            "type": "object",
            "required": [
              "mode",
              "seeded",
              "values"
            ],
            "properties": {
              "mode": {
                "type": "string"
              },
              "seeded": {
                "type": "boolean"
              },
              "seed": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "values": {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "description": "Generated values as text, in order."
              },
              "numbers": {
                "type": "array",
                "items": {
                  "type": "number"
                }
              },
              "dice": {
                "type": "object",
                "properties": {
                  "notation": {
                    "type": "string"
                  },
                  "rolls": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "dice": {
                          "type": "array",
                          "items": {
                            "type": "integer"
                          }
                        },
                        "total": {
                          "type": "integer"
                        }
                      }
                    }
                  },
                  "min": {
                    "type": "integer"
                  },
                  "max": {
                    "type": "integer"
                  },
                  "mean": {
                    "type": "number"
                  }
                }
              },
              "heads": {
                "type": "integer"
              },
              "teams": {
                "type": "array",
                "items": {
                  "type": "object",
                  "required": [
                    "team",
                    "members"
                  ],
                  "properties": {
                    "team": {
                      "type": "integer"
                    },
                    "members": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "totalSkill": {
                      "type": [
                        "number",
                        "null"
                      ]
                    }
                  }
                }
              },
              "stats": {
                "type": "object",
                "properties": {
                  "sum": {
                    "type": "number"
                  },
                  "mean": {
                    "type": "number"
                  },
                  "min": {
                    "type": "number"
                  },
                  "max": {
                    "type": "number"
                  }
                }
              }
            }
          },
          "units": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            }
          },
          "assumptions": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "sources": {
            "type": "array",
            "items": {
              "type": "object",
              "additionalProperties": false,
              "required": [
                "title"
              ],
              "properties": {
                "title": {
                  "type": "string"
                },
                "url": {
                  "type": "string"
                },
                "publisher": {
                  "type": "string"
                },
                "retrievedAt": {
                  "type": "string"
                },
                "note": {
                  "type": "string"
                }
              }
            }
          },
          "warnings": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "presentation": {
            "type": "array",
            "items": {
              "type": "object"
            },
            "description": "Display blocks used by the human page (same data)."
          },
          "links": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "human",
              "api",
              "openapi",
              "methodology"
            ],
            "properties": {
              "human": {
                "type": "string",
                "format": "uri"
              },
              "api": {
                "type": "string",
                "format": "uri"
              },
              "openapi": {
                "type": "string",
                "format": "uri"
              },
              "methodology": {
                "type": "string",
                "format": "uri"
              }
            }
          }
        }
      },
      "RunningPaceCalculatorInput": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "solveFor": {
            "title": "Calculate",
            "type": "string",
            "enum": [
              "pace",
              "time",
              "distance"
            ],
            "default": "pace"
          },
          "preset": {
            "title": "Race distance",
            "type": "string",
            "enum": [
              "custom",
              "1k",
              "mile",
              "5k",
              "10k",
              "15k",
              "10mi",
              "half",
              "marathon",
              "50k"
            ],
            "default": "10k"
          },
          "distance": {
            "title": "Custom distance",
            "description": "Used when the race distance is \"Custom distance\".",
            "type": "number",
            "minimum": 0.001,
            "maximum": 1000
          },
          "distanceUnit": {
            "title": "Distance unit",
            "type": "string",
            "enum": [
              "km",
              "mi",
              "m"
            ],
            "default": "km"
          },
          "time": {
            "title": "Time",
            "description": "h:mm:ss or mm:ss, e.g. 1:45:00 or 24:30.",
            "type": "string",
            "maxLength": 20
          },
          "pace": {
            "title": "Pace",
            "description": "mm:ss per km or per mile, e.g. 5:30.",
            "type": "string",
            "maxLength": 20
          },
          "paceUnit": {
            "title": "Pace per",
            "type": "string",
            "enum": [
              "km",
              "mi"
            ],
            "default": "km"
          },
          "splitUnit": {
            "title": "Splits every",
            "type": "string",
            "enum": [
              "km",
              "mi"
            ],
            "default": "km"
          },
          "splitStrategy": {
            "title": "Split strategy",
            "type": "string",
            "enum": [
              "even",
              "negative",
              "positive"
            ],
            "default": "even"
          },
          "splitPercent": {
            "title": "Pace change start → finish",
            "description": "For negative/positive splits: how much the pace changes from the start to the finish, spread evenly.",
            "type": "number",
            "minimum": 0,
            "maximum": 20,
            "default": 3,
            "x-unit": "%"
          },
          "lapMeters": {
            "title": "Track lap length",
            "type": "number",
            "minimum": 50,
            "maximum": 1000,
            "default": 400,
            "x-unit": "m"
          }
        },
        "required": []
      },
      "RunningPaceCalculatorResult": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "schemaVersion",
          "tool",
          "computedAt",
          "inputs",
          "summary",
          "result",
          "units",
          "assumptions",
          "sources",
          "warnings",
          "presentation",
          "links"
        ],
        "properties": {
          "schemaVersion": {
            "type": "string",
            "const": "1.0"
          },
          "tool": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "id",
              "name",
              "version"
            ],
            "properties": {
              "id": {
                "type": "string"
              },
              "name": {
                "type": "string"
              },
              "version": {
                "type": "string"
              }
            }
          },
          "computedAt": {
            "type": "string",
            "format": "date-time"
          },
          "inputs": {
            "type": "object",
            "description": "Normalised inputs actually used, including applied defaults."
          },
          "summary": {
            "type": "string"
          },
          "result": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "distanceMeters",
              "distanceKm",
              "distanceMiles",
              "timeSeconds",
              "time",
              "paceSecPerKm",
              "paceSecPerMile",
              "pacePerKm",
              "pacePerMile",
              "speedKmh",
              "speedMph",
              "splits",
              "halves",
              "predictions",
              "laps"
            ],
            "properties": {
              "distanceMeters": {
                "type": "number"
              },
              "distanceKm": {
                "type": "number"
              },
              "distanceMiles": {
                "type": "number"
              },
              "timeSeconds": {
                "type": "number"
              },
              "time": {
                "type": "string"
              },
              "paceSecPerKm": {
                "type": "number"
              },
              "paceSecPerMile": {
                "type": "number"
              },
              "pacePerKm": {
                "type": "string"
              },
              "pacePerMile": {
                "type": "string"
              },
              "speedKmh": {
                "type": "number"
              },
              "speedMph": {
                "type": "number"
              },
              "splits": {
                "type": "array",
                "items": {
                  "type": "object",
                  "additionalProperties": false,
                  "required": [
                    "split",
                    "atDistance",
                    "splitTime",
                    "splitSeconds",
                    "cumulative",
                    "cumulativeSeconds",
                    "pace"
                  ],
                  "properties": {
                    "split": {
                      "type": "integer"
                    },
                    "atDistance": {
                      "type": "number"
                    },
                    "splitTime": {
                      "type": "string"
                    },
                    "splitSeconds": {
                      "type": "number"
                    },
                    "cumulative": {
                      "type": "string"
                    },
                    "cumulativeSeconds": {
                      "type": "number"
                    },
                    "pace": {
                      "type": "string"
                    }
                  }
                }
              },
              "halves": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "first",
                  "second"
                ],
                "properties": {
                  "first": {
                    "type": "string"
                  },
                  "second": {
                    "type": "string"
                  }
                }
              },
              "predictions": {
                "type": "array",
                "items": {
                  "type": "object",
                  "additionalProperties": false,
                  "required": [
                    "race",
                    "meters",
                    "seconds",
                    "time",
                    "pacePerKm",
                    "pacePerMile",
                    "inRange"
                  ],
                  "properties": {
                    "race": {
                      "type": "string"
                    },
                    "meters": {
                      "type": "number"
                    },
                    "seconds": {
                      "type": "number"
                    },
                    "time": {
                      "type": "string"
                    },
                    "pacePerKm": {
                      "type": "string"
                    },
                    "pacePerMile": {
                      "type": "string"
                    },
                    "inRange": {
                      "type": "boolean",
                      "description": "Both times within Riegel's 3.5–230 minute endurance range."
                    }
                  }
                }
              },
              "laps": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "lapMeters",
                  "lapTime",
                  "lapSeconds",
                  "halfLapTime",
                  "lapsForDistance"
                ],
                "properties": {
                  "lapMeters": {
                    "type": "number"
                  },
                  "lapTime": {
                    "type": "string"
                  },
                  "lapSeconds": {
                    "type": "number"
                  },
                  "halfLapTime": {
                    "type": "string"
                  },
                  "lapsForDistance": {
                    "type": "number"
                  }
                }
              }
            }
          },
          "units": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            }
          },
          "assumptions": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "sources": {
            "type": "array",
            "items": {
              "type": "object",
              "additionalProperties": false,
              "required": [
                "title"
              ],
              "properties": {
                "title": {
                  "type": "string"
                },
                "url": {
                  "type": "string"
                },
                "publisher": {
                  "type": "string"
                },
                "retrievedAt": {
                  "type": "string"
                },
                "note": {
                  "type": "string"
                }
              }
            }
          },
          "warnings": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "presentation": {
            "type": "array",
            "items": {
              "type": "object"
            },
            "description": "Display blocks used by the human page (same data)."
          },
          "links": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "human",
              "api",
              "openapi",
              "methodology"
            ],
            "properties": {
              "human": {
                "type": "string",
                "format": "uri"
              },
              "api": {
                "type": "string",
                "format": "uri"
              },
              "openapi": {
                "type": "string",
                "format": "uri"
              },
              "methodology": {
                "type": "string",
                "format": "uri"
              }
            }
          }
        }
      },
      "SportsTravelFatigueEstimatorInput": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "homeBase": {
            "title": "Starting city",
            "description": "A city from the bundled list (118 major sports cities) or coordinates like 42.36,-71.06.",
            "type": "string",
            "maxLength": 80,
            "default": "Boston"
          },
          "homeTimeZone": {
            "title": "Starting time zone",
            "description": "Only needed for coordinates: an IANA zone like America/New_York or an offset like -5.",
            "type": "string",
            "maxLength": 40
          },
          "games": {
            "title": "Games",
            "description": "In date order. Location: a listed city or latitude,longitude. Time zone is only needed for coordinates.",
            "type": "array",
            "minItems": 1,
            "maxItems": 100,
            "items": {
              "type": "object",
              "additionalProperties": false,
              "properties": {
                "date": {
                  "title": "Date",
                  "type": "string",
                  "format": "date"
                },
                "time": {
                  "title": "Start time (local)",
                  "type": "string",
                  "pattern": "^([01]\\d|2[0-3]):[0-5]\\d$"
                },
                "location": {
                  "title": "City or lat,lon",
                  "type": "string",
                  "maxLength": 80
                },
                "timeZone": {
                  "title": "Time zone",
                  "type": "string",
                  "maxLength": 40
                },
                "label": {
                  "title": "Opponent / note",
                  "type": "string",
                  "maxLength": 60
                }
              },
              "required": [
                "date",
                "location"
              ]
            }
          }
        },
        "required": [
          "homeBase",
          "games"
        ]
      },
      "SportsTravelFatigueEstimatorResult": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "schemaVersion",
          "tool",
          "computedAt",
          "inputs",
          "summary",
          "result",
          "units",
          "assumptions",
          "sources",
          "warnings",
          "presentation",
          "links"
        ],
        "properties": {
          "schemaVersion": {
            "type": "string",
            "const": "1.0"
          },
          "tool": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "id",
              "name",
              "version"
            ],
            "properties": {
              "id": {
                "type": "string"
              },
              "name": {
                "type": "string"
              },
              "version": {
                "type": "string"
              }
            }
          },
          "computedAt": {
            "type": "string",
            "format": "date-time"
          },
          "inputs": {
            "type": "object",
            "description": "Normalised inputs actually used, including applied defaults."
          },
          "summary": {
            "type": "string"
          },
          "result": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "games",
              "totals",
              "weights",
              "cityListVersion"
            ],
            "properties": {
              "games": {
                "type": "array",
                "items": {
                  "type": "object",
                  "additionalProperties": false,
                  "required": [
                    "n",
                    "date",
                    "location",
                    "label",
                    "from",
                    "km",
                    "timeZone",
                    "utcOffsetHours",
                    "shiftHours",
                    "direction",
                    "daysSincePrevious",
                    "restDays",
                    "backToBack",
                    "gamesLast7Days",
                    "components",
                    "score",
                    "band"
                  ],
                  "properties": {
                    "n": {
                      "type": "integer"
                    },
                    "date": {
                      "type": "string"
                    },
                    "location": {
                      "type": "string"
                    },
                    "label": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "from": {
                      "type": "string"
                    },
                    "km": {
                      "type": "number"
                    },
                    "timeZone": {
                      "type": "string"
                    },
                    "utcOffsetHours": {
                      "type": "number"
                    },
                    "shiftHours": {
                      "type": "number",
                      "description": "Positive = clocks ahead (eastward), negative = westward."
                    },
                    "direction": {
                      "type": "string",
                      "enum": [
                        "east",
                        "west",
                        "none"
                      ]
                    },
                    "daysSincePrevious": {
                      "type": [
                        "integer",
                        "null"
                      ]
                    },
                    "restDays": {
                      "type": [
                        "integer",
                        "null"
                      ]
                    },
                    "backToBack": {
                      "type": "boolean"
                    },
                    "gamesLast7Days": {
                      "type": "integer"
                    },
                    "components": {
                      "type": "object",
                      "additionalProperties": false,
                      "required": [
                        "travel",
                        "timezone",
                        "residualHours",
                        "rest",
                        "density"
                      ],
                      "properties": {
                        "travel": {
                          "type": "number"
                        },
                        "timezone": {
                          "type": "number"
                        },
                        "residualHours": {
                          "type": "number"
                        },
                        "rest": {
                          "type": "number"
                        },
                        "density": {
                          "type": "number"
                        }
                      }
                    },
                    "score": {
                      "type": "number"
                    },
                    "band": {
                      "type": "string",
                      "enum": [
                        "low",
                        "moderate",
                        "high",
                        "very high"
                      ]
                    }
                  }
                }
              },
              "totals": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "km",
                  "timeZoneHoursCrossed",
                  "backToBacks",
                  "averageScore",
                  "maxScore",
                  "hardestGame"
                ],
                "properties": {
                  "km": {
                    "type": "number"
                  },
                  "timeZoneHoursCrossed": {
                    "type": "number"
                  },
                  "backToBacks": {
                    "type": "integer"
                  },
                  "averageScore": {
                    "type": "number"
                  },
                  "maxScore": {
                    "type": "number"
                  },
                  "hardestGame": {
                    "type": "integer"
                  }
                }
              },
              "weights": {
                "type": "object"
              },
              "cityListVersion": {
                "type": "string"
              }
            }
          },
          "units": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            }
          },
          "assumptions": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "sources": {
            "type": "array",
            "items": {
              "type": "object",
              "additionalProperties": false,
              "required": [
                "title"
              ],
              "properties": {
                "title": {
                  "type": "string"
                },
                "url": {
                  "type": "string"
                },
                "publisher": {
                  "type": "string"
                },
                "retrievedAt": {
                  "type": "string"
                },
                "note": {
                  "type": "string"
                }
              }
            }
          },
          "warnings": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "presentation": {
            "type": "array",
            "items": {
              "type": "object"
            },
            "description": "Display blocks used by the human page (same data)."
          },
          "links": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "human",
              "api",
              "openapi",
              "methodology"
            ],
            "properties": {
              "human": {
                "type": "string",
                "format": "uri"
              },
              "api": {
                "type": "string",
                "format": "uri"
              },
              "openapi": {
                "type": "string",
                "format": "uri"
              },
              "methodology": {
                "type": "string",
                "format": "uri"
              }
            }
          }
        }
      },
      "TipBillSplitCalculatorInput": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "symbol": {
            "title": "Currency symbol",
            "description": "Display only, e.g. €, $, £, CHF.",
            "type": "string",
            "maxLength": 4,
            "default": "€"
          },
          "billAmount": {
            "title": "Bill amount",
            "description": "The amount printed on the bill (before tip).",
            "type": "number",
            "minimum": 0.01,
            "maximum": 10000000,
            "default": 84.5
          },
          "taxMode": {
            "title": "Tax",
            "type": "string",
            "enum": [
              "included",
              "added",
              "none"
            ],
            "default": "included"
          },
          "taxPercent": {
            "title": "Tax rate",
            "type": "number",
            "minimum": 0,
            "maximum": 100,
            "default": 19,
            "x-unit": "%"
          },
          "tipMode": {
            "title": "Tip given as",
            "type": "string",
            "enum": [
              "percent",
              "amount"
            ],
            "default": "percent"
          },
          "tipPercent": {
            "title": "Tip percentage",
            "type": "number",
            "minimum": 0,
            "maximum": 100,
            "default": 10,
            "x-unit": "%"
          },
          "tipAmount": {
            "title": "Tip amount",
            "type": "number",
            "minimum": 0,
            "default": 0
          },
          "tipBase": {
            "title": "Tip calculated on",
            "type": "string",
            "enum": [
              "pre-tax",
              "post-tax"
            ],
            "default": "post-tax"
          },
          "splitMode": {
            "title": "Split",
            "type": "string",
            "enum": [
              "equal",
              "shares",
              "items"
            ],
            "default": "equal"
          },
          "people": {
            "title": "Number of people (equal split)",
            "description": "Used when the list of people below is empty.",
            "type": "integer",
            "minimum": 1,
            "maximum": 100,
            "default": 4
          },
          "persons": {
            "title": "People",
            "description": "Optional for an equal split. Shares: weight per person (default 1). Items: the bill amount of what each person ordered, in the same terms as the bill (with tax if tax is included); anything not assigned is shared equally.",
            "type": "array",
            "maxItems": 40,
            "items": {
              "type": "object",
              "additionalProperties": false,
              "properties": {
                "name": {
                  "title": "Name",
                  "type": "string",
                  "maxLength": 40
                },
                "shares": {
                  "title": "Shares",
                  "type": "number",
                  "minimum": 0,
                  "maximum": 100
                },
                "items": {
                  "title": "Ordered items",
                  "type": "number",
                  "minimum": 0
                }
              },
              "required": [
                "name"
              ]
            }
          },
          "rounding": {
            "title": "Rounding per person",
            "description": "Any rounding difference is added to (or taken from) the tip.",
            "type": "string",
            "enum": [
              "none",
              "up-0.1",
              "up-0.5",
              "up-1",
              "up-5",
              "nearest-0.05"
            ],
            "default": "none"
          }
        },
        "required": [
          "billAmount"
        ]
      },
      "TipBillSplitCalculatorResult": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "schemaVersion",
          "tool",
          "computedAt",
          "inputs",
          "summary",
          "result",
          "units",
          "assumptions",
          "sources",
          "warnings",
          "presentation",
          "links"
        ],
        "properties": {
          "schemaVersion": {
            "type": "string",
            "const": "1.0"
          },
          "tool": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "id",
              "name",
              "version"
            ],
            "properties": {
              "id": {
                "type": "string"
              },
              "name": {
                "type": "string"
              },
              "version": {
                "type": "string"
              }
            }
          },
          "computedAt": {
            "type": "string",
            "format": "date-time"
          },
          "inputs": {
            "type": "object",
            "description": "Normalised inputs actually used, including applied defaults."
          },
          "summary": {
            "type": "string"
          },
          "result": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "symbol",
              "subtotalBeforeTax",
              "tax",
              "billTotal",
              "tipBase",
              "tip",
              "tipPercentOfBill",
              "grandTotal",
              "collected",
              "roundingAdded",
              "effectiveTip",
              "effectiveTipPercent",
              "people"
            ],
            "properties": {
              "symbol": {
                "type": "string"
              },
              "subtotalBeforeTax": {
                "type": "number"
              },
              "tax": {
                "type": "number"
              },
              "billTotal": {
                "type": "number",
                "description": "Bill including tax, before tip."
              },
              "tipBase": {
                "type": "number",
                "description": "Amount the tip percentage was applied to."
              },
              "tip": {
                "type": "number",
                "description": "Tip before per-person rounding."
              },
              "tipPercentOfBill": {
                "type": "number",
                "description": "Tip as % of the bill including tax."
              },
              "grandTotal": {
                "type": "number",
                "description": "Bill including tax + tip, before rounding."
              },
              "collected": {
                "type": "number",
                "description": "Sum of what everyone pays after rounding."
              },
              "roundingAdded": {
                "type": "number",
                "description": "collected − grandTotal (goes to the tip)."
              },
              "effectiveTip": {
                "type": "number"
              },
              "effectiveTipPercent": {
                "type": "number",
                "description": "Effective tip as % of the tip base."
              },
              "people": {
                "type": "array",
                "items": {
                  "type": "object",
                  "additionalProperties": false,
                  "required": [
                    "name",
                    "weight",
                    "billShare",
                    "taxShare",
                    "tipShare",
                    "exact",
                    "pays"
                  ],
                  "properties": {
                    "name": {
                      "type": "string"
                    },
                    "weight": {
                      "type": "number"
                    },
                    "billShare": {
                      "type": "number"
                    },
                    "taxShare": {
                      "type": "number"
                    },
                    "tipShare": {
                      "type": "number"
                    },
                    "exact": {
                      "type": "number"
                    },
                    "pays": {
                      "type": "number"
                    }
                  }
                }
              }
            }
          },
          "units": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            }
          },
          "assumptions": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "sources": {
            "type": "array",
            "items": {
              "type": "object",
              "additionalProperties": false,
              "required": [
                "title"
              ],
              "properties": {
                "title": {
                  "type": "string"
                },
                "url": {
                  "type": "string"
                },
                "publisher": {
                  "type": "string"
                },
                "retrievedAt": {
                  "type": "string"
                },
                "note": {
                  "type": "string"
                }
              }
            }
          },
          "warnings": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "presentation": {
            "type": "array",
            "items": {
              "type": "object"
            },
            "description": "Display blocks used by the human page (same data)."
          },
          "links": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "human",
              "api",
              "openapi",
              "methodology"
            ],
            "properties": {
              "human": {
                "type": "string",
                "format": "uri"
              },
              "api": {
                "type": "string",
                "format": "uri"
              },
              "openapi": {
                "type": "string",
                "format": "uri"
              },
              "methodology": {
                "type": "string",
                "format": "uri"
              }
            }
          }
        }
      }
    }
  }
}