{
  "openapi": "3.1.0",
  "info": {
    "title": "Far Better Off API",
    "version": "1.0.0",
    "summary": "Free US personal-finance math as JSON. No key, no signup, no tracking.",
    "description": "The same `@calcwise/finance` functions that power farbetteroff.com, exposed as JSON so the API and the site can never disagree.\n\n**No authentication.** There are no keys, accounts, tiers or quotas to apply for. Send a request.\n\n**Every endpoint is a pure function.** The same URL returns the same answer forever, nothing is stored, and your inputs are never logged.\n\n**Rate limit:** 60 requests per 60 seconds per IP, best effort. Responses are cacheable, so caching removes almost all of it.\n\n**CORS** is open to every origin. There are no cookies and no credentials, so there is nothing an open policy could leak.\n\nNumbers are educational estimates, not financial advice. Figures that come from a regulator cite that regulator in the endpoint description.",
    "license": {
      "name": "MIT",
      "identifier": "MIT"
    },
    "contact": {
      "name": "Far Better Off",
      "url": "https://farbetteroff.com/api",
      "email": "hellofarbetteroff@gmail.com"
    }
  },
  "externalDocs": {
    "description": "Human-readable API docs",
    "url": "https://farbetteroff.com/api"
  },
  "security": [],
  "servers": [
    {
      "url": "https://farbetteroff.com/api/v1",
      "description": "Production"
    }
  ],
  "tags": [
    {
      "name": "calculators",
      "description": "One endpoint per Far Better Off calculator."
    },
    {
      "name": "utilities",
      "description": "Small standalone helpers."
    },
    {
      "name": "datasets",
      "description": "Figures Far Better Off publishes, rather than functions it exposes. No parameters, and the same URL returns a different number when a source series publishes — so read `asOf` and honour the Cache-Control."
    }
  ],
  "paths": {
    "/pay": {
      "get": {
        "tags": [
          "calculators"
        ],
        "summary": "Pay, converted",
        "description": "One wage restated in every period: per hour, per day, per week, every two weeks, twice a month, per month and per year — in either direction.\n\nGross pay, before tax and deductions. /api/v1/paycheck takes federal income tax and FICA out of the annual figure; /api/v1/state-tax adds state wage tax where Far Better Off models it.\n\nHourly, daily and weekly follow the schedule you pass, because a real schedule changes them: 40 hours a week for 52 weeks by default, a 2,080-hour year.\n\nHours past 40 carry the overtime premium, because a paycheck does: 29 U.S.C. § 207(a)(1) requires at least one and a half times the regular rate past 40 hours in a workweek, so `amount=25&per=hour&hoursPerWeek=50` is **$71,500** a year — 40 × $25 plus 10 × $37.50 — and not the $65,000 straight-lining gives. That is the default because the salary ↔ hourly calculator and the /pay pages have answered it that way since 2026-09-12, and one wage cannot have two answers. Send `overtimeMultiplier=1` for an exempt salaried schedule that earns no premium.\n\nSent a salary rather than an hourly rate, the yearly total is whatever you sent — a salary is already the whole year — and the premium changes what it is worth by the hour instead: `hourly` is the average across every hour, `baseHourly` is what an hourly job would have to pay for the first 40. A $60,000 salary at 50 hours a week averages $23.08 an hour but only needs a **$20.98** base rate to match, because ten of those hours pay 1.5×.\n\nThis is the federal floor and nothing else. California, Alaska and a handful of other states add a daily overtime rule on top of the weekly one, and a contract can start the premium sooner — `overtimeAfterHours` is there for both. Whether a particular job is exempt from the premium at all is a question about the job, not about the arithmetic, and this endpoint does not answer it.\n\nMonthly, semi-monthly and biweekly are fixed payroll counts — 12, 24 and 26 pay periods — so they do not move when the schedule does.\n\n26 biweekly periods is the convention, not the calendar: 52 weeks is exactly 26 fortnights, but a 365-day year is 26.07, so some years contain 27 biweekly paychecks.\n\nFederal pay conversion divides an annual rate by 2,087 hours rather than 2,080 (5 U.S.C. § 5504(b), which averages the leap-year cycle in), so a federal annual rate converted here reads about 0.34% high.",
        "externalDocs": {
          "description": "The pay, converted calculator on farbetteroff.com",
          "url": "https://farbetteroff.com/calculators/salary-to-hourly-calculator"
        },
        "responses": {
          "200": {
            "$ref": "#/components/responses/pay-result"
          },
          "400": {
            "description": "A parameter is missing, mistyped, out of range or unknown — all of them are listed at once. A name that is one or two edits from a real parameter is answered with the one it probably meant; an ambiguous one is answered with the accepted list instead of a guess.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such endpoint. The response lists every valid slug.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "More than 60 requests in 60 seconds from one IP.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "A bug on our side. The inputs are never at fault for this one.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "operationId": "get-pay",
        "parameters": [
          {
            "name": "amount",
            "in": "query",
            "required": true,
            "description": "The pay figure you have. Accepts number, US dollars, between 0 and 1000000000.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 1000000000,
              "x-unit": "usd",
              "examples": [
                25
              ]
            }
          },
          {
            "name": "per",
            "in": "query",
            "required": true,
            "description": "The period that amount is for. `biweek` is every two weeks (26 paychecks a year), `semimonth` twice a month (24). Accepts one of hour, day, week, biweek, semimonth, month, year. 13 of the listed spellings are aliases: they are resolved to one of 7 canonical values (`x-canonical-values`) before the answer echoes them. Matching ignores case.",
            "schema": {
              "type": "string",
              "enum": [
                "hour",
                "day",
                "week",
                "biweek",
                "semimonth",
                "month",
                "year",
                "hourly",
                "per-hour",
                "daily",
                "weekly",
                "biweekly",
                "fortnight",
                "fortnightly",
                "semimonthly",
                "semi-monthly",
                "monthly",
                "yearly",
                "annual",
                "annually"
              ],
              "x-canonical-values": [
                "hour",
                "day",
                "week",
                "biweek",
                "semimonth",
                "month",
                "year"
              ],
              "examples": [
                "hour"
              ]
            }
          },
          {
            "name": "hoursPerWeek",
            "in": "query",
            "required": false,
            "description": "Hours worked in a week. Accepts number, a count, between 0.5 and 168.",
            "schema": {
              "type": "number",
              "minimum": 0.5,
              "maximum": 168,
              "default": 40,
              "x-unit": "count"
            }
          },
          {
            "name": "weeksPerYear",
            "in": "query",
            "required": false,
            "description": "Weeks **paid** in a year. Drop it for unpaid time off: two weeks off is 50. Accepts number, a count, between 1 and 52.",
            "schema": {
              "type": "number",
              "minimum": 1,
              "maximum": 52,
              "default": 52,
              "x-unit": "count"
            }
          },
          {
            "name": "daysPerWeek",
            "in": "query",
            "required": false,
            "description": "Days worked in a week, which is what the daily figure divides by. Accepts number, a count, between 1 and 7.",
            "schema": {
              "type": "number",
              "minimum": 1,
              "maximum": 7,
              "default": 5,
              "x-unit": "count"
            }
          },
          {
            "name": "overtimeAfterHours",
            "in": "query",
            "required": false,
            "description": "Hours in the week after which the premium starts. 40 is the federal floor; a contract or a state rule can start it sooner. Accepts number, a count, between 1 and 168.",
            "schema": {
              "type": "number",
              "minimum": 1,
              "maximum": 168,
              "default": 40,
              "x-unit": "count"
            }
          },
          {
            "name": "overtimeMultiplier",
            "in": "query",
            "required": false,
            "description": "What an hour past the threshold pays, as a multiple of the base rate. Send `1` for a schedule with no premium — a salaried exempt job — and every figure is straight time again. Accepts number, between 1 and 3.",
            "schema": {
              "type": "number",
              "minimum": 1,
              "maximum": 3,
              "default": 1.5
            }
          }
        ]
      },
      "post": {
        "tags": [
          "calculators"
        ],
        "summary": "Pay, converted",
        "description": "One wage restated in every period: per hour, per day, per week, every two weeks, twice a month, per month and per year — in either direction.\n\nGross pay, before tax and deductions. /api/v1/paycheck takes federal income tax and FICA out of the annual figure; /api/v1/state-tax adds state wage tax where Far Better Off models it.\n\nHourly, daily and weekly follow the schedule you pass, because a real schedule changes them: 40 hours a week for 52 weeks by default, a 2,080-hour year.\n\nHours past 40 carry the overtime premium, because a paycheck does: 29 U.S.C. § 207(a)(1) requires at least one and a half times the regular rate past 40 hours in a workweek, so `amount=25&per=hour&hoursPerWeek=50` is **$71,500** a year — 40 × $25 plus 10 × $37.50 — and not the $65,000 straight-lining gives. That is the default because the salary ↔ hourly calculator and the /pay pages have answered it that way since 2026-09-12, and one wage cannot have two answers. Send `overtimeMultiplier=1` for an exempt salaried schedule that earns no premium.\n\nSent a salary rather than an hourly rate, the yearly total is whatever you sent — a salary is already the whole year — and the premium changes what it is worth by the hour instead: `hourly` is the average across every hour, `baseHourly` is what an hourly job would have to pay for the first 40. A $60,000 salary at 50 hours a week averages $23.08 an hour but only needs a **$20.98** base rate to match, because ten of those hours pay 1.5×.\n\nThis is the federal floor and nothing else. California, Alaska and a handful of other states add a daily overtime rule on top of the weekly one, and a contract can start the premium sooner — `overtimeAfterHours` is there for both. Whether a particular job is exempt from the premium at all is a question about the job, not about the arithmetic, and this endpoint does not answer it.\n\nMonthly, semi-monthly and biweekly are fixed payroll counts — 12, 24 and 26 pay periods — so they do not move when the schedule does.\n\n26 biweekly periods is the convention, not the calendar: 52 weeks is exactly 26 fortnights, but a 365-day year is 26.07, so some years contain 27 biweekly paychecks.\n\nFederal pay conversion divides an annual rate by 2,087 hours rather than 2,080 (5 U.S.C. § 5504(b), which averages the leap-year cycle in), so a federal annual rate converted here reads about 0.34% high.\n\nIdentical to the GET, with the parameters in a JSON object body.",
        "externalDocs": {
          "description": "The pay, converted calculator on farbetteroff.com",
          "url": "https://farbetteroff.com/calculators/salary-to-hourly-calculator"
        },
        "responses": {
          "200": {
            "$ref": "#/components/responses/pay-result"
          },
          "400": {
            "description": "A parameter is missing, mistyped, out of range or unknown — all of them are listed at once. A name that is one or two edits from a real parameter is answered with the one it probably meant; an ambiguous one is answered with the accepted list instead of a guess.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such endpoint. The response lists every valid slug.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "More than 60 requests in 60 seconds from one IP.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "A bug on our side. The inputs are never at fault for this one.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "operationId": "post-pay",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "amount",
                  "per"
                ],
                "properties": {
                  "amount": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 1000000000,
                    "x-unit": "usd",
                    "examples": [
                      25
                    ],
                    "description": "The pay figure you have. Accepts number, US dollars, between 0 and 1000000000."
                  },
                  "per": {
                    "type": "string",
                    "enum": [
                      "hour",
                      "day",
                      "week",
                      "biweek",
                      "semimonth",
                      "month",
                      "year",
                      "hourly",
                      "per-hour",
                      "daily",
                      "weekly",
                      "biweekly",
                      "fortnight",
                      "fortnightly",
                      "semimonthly",
                      "semi-monthly",
                      "monthly",
                      "yearly",
                      "annual",
                      "annually"
                    ],
                    "x-canonical-values": [
                      "hour",
                      "day",
                      "week",
                      "biweek",
                      "semimonth",
                      "month",
                      "year"
                    ],
                    "examples": [
                      "hour"
                    ],
                    "description": "The period that amount is for. `biweek` is every two weeks (26 paychecks a year), `semimonth` twice a month (24). Accepts one of hour, day, week, biweek, semimonth, month, year. 13 of the listed spellings are aliases: they are resolved to one of 7 canonical values (`x-canonical-values`) before the answer echoes them. Matching ignores case."
                  },
                  "hoursPerWeek": {
                    "type": "number",
                    "minimum": 0.5,
                    "maximum": 168,
                    "default": 40,
                    "x-unit": "count",
                    "description": "Hours worked in a week. Accepts number, a count, between 0.5 and 168."
                  },
                  "weeksPerYear": {
                    "type": "number",
                    "minimum": 1,
                    "maximum": 52,
                    "default": 52,
                    "x-unit": "count",
                    "description": "Weeks **paid** in a year. Drop it for unpaid time off: two weeks off is 50. Accepts number, a count, between 1 and 52."
                  },
                  "daysPerWeek": {
                    "type": "number",
                    "minimum": 1,
                    "maximum": 7,
                    "default": 5,
                    "x-unit": "count",
                    "description": "Days worked in a week, which is what the daily figure divides by. Accepts number, a count, between 1 and 7."
                  },
                  "overtimeAfterHours": {
                    "type": "number",
                    "minimum": 1,
                    "maximum": 168,
                    "default": 40,
                    "x-unit": "count",
                    "description": "Hours in the week after which the premium starts. 40 is the federal floor; a contract or a state rule can start it sooner. Accepts number, a count, between 1 and 168."
                  },
                  "overtimeMultiplier": {
                    "type": "number",
                    "minimum": 1,
                    "maximum": 3,
                    "default": 1.5,
                    "description": "What an hour past the threshold pays, as a multiple of the base rate. Send `1` for a schedule with no premium — a salaried exempt job — and every figure is straight time again. Accepts number, between 1 and 3."
                  }
                }
              }
            }
          }
        }
      }
    },
    "/paycheck": {
      "get": {
        "tags": [
          "calculators"
        ],
        "summary": "Take-home pay (federal)",
        "description": "Federal income tax bracket by bracket, Social Security, Medicare and what is left of a salary — per year, per month and per paycheck.\n\nFederal only. No state or local income tax is included, so in 41 states and D.C. real take-home pay is lower than takeHome. /api/v1/state-tax answers the state line where that is a fact rather than a guess: 0 in the 9 states that tax no wages, a computed figure in the 16 with one statutory rate and in California, New Jersey, New York and Virginia, whose published bracket schedules are modelled, and null in the other 22, whose brackets Far Better Off has not verified and will not approximate. Income tax is not the whole of it either: 10 states withhold a disability or paid-leave contribution from the same wages, which the same endpoint answers in \"payrollContributions\" — so takeHome is high by $975 a year for a Californian on $75,000 before a cent of state income tax is counted, and high in Washington, which levies no income tax at all.\n\n2026 figures: brackets and standard deduction from IRS Rev. Proc. 2025-32; Social Security wage base $184,500 and the FICA rates from IRS Topic no. 751; the $24,500 elective-deferral limit and its $8,000 and $11,250 catch-ups from IRS Notice 2025-67.\n\ncontribPercent is capped at the § 402(g) limit, so a percent that would exceed it returns the contribution a plan could actually take and flags contributionLimited. Traditional and Roth deferrals share the one limit; an employer match is outside it and is not modelled here.\n\nThis is the employee half of FICA. It is not self-employment tax, and it does not model W-4 allowances, pre-tax health or HSA premiums, credits, or income other than wages.",
        "externalDocs": {
          "description": "The take-home pay (federal) calculator on farbetteroff.com",
          "url": "https://farbetteroff.com/calculators/take-home-paycheck-calculator"
        },
        "responses": {
          "200": {
            "$ref": "#/components/responses/paycheck-result"
          },
          "400": {
            "description": "A parameter is missing, mistyped, out of range or unknown — all of them are listed at once. A name that is one or two edits from a real parameter is answered with the one it probably meant; an ambiguous one is answered with the accepted list instead of a guess.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such endpoint. The response lists every valid slug.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "More than 60 requests in 60 seconds from one IP.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "A bug on our side. The inputs are never at fault for this one.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "operationId": "get-paycheck",
        "parameters": [
          {
            "name": "salary",
            "in": "query",
            "required": true,
            "description": "Gross annual salary. Accepts number, US dollars, between 0 and 1000000000.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 1000000000,
              "x-unit": "usd",
              "examples": [
                75000
              ]
            }
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "Filing status: single, married (filing jointly), hoh (head of household) or mfs (married filing separately). Accepts one of single, married, hoh, mfs.",
            "schema": {
              "type": "string",
              "enum": [
                "single",
                "married",
                "hoh",
                "mfs"
              ],
              "default": "single",
              "examples": [
                "single"
              ]
            }
          },
          {
            "name": "contribPercent",
            "in": "query",
            "required": false,
            "description": "401(k)/403(b) contribution as a percent of salary. See contribType for which side of the tax line it falls on. Accepts number, a percentage, so 6.5 means 6.5%, between 0 and 100.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 100,
              "default": 0,
              "x-unit": "percent",
              "examples": [
                5
              ]
            }
          },
          {
            "name": "contribType",
            "in": "query",
            "required": false,
            "description": "traditional: an elective deferral, exempt from income tax but not from FICA. roth: a designated Roth contribution, which the IRS includes in gross income when it is earned, so it reduces no tax base at all and only takeHome moves. Accepts one of traditional, roth. 10 of the listed spellings are aliases: they are resolved to one of 2 canonical values (`x-canonical-values`) before the answer echoes them. Matching ignores case.",
            "schema": {
              "type": "string",
              "enum": [
                "traditional",
                "roth",
                "pretax",
                "pre-tax",
                "pre tax",
                "deferral",
                "trad",
                "roth 401k",
                "roth 401(k)",
                "designated roth",
                "after-tax",
                "aftertax"
              ],
              "x-canonical-values": [
                "traditional",
                "roth"
              ],
              "default": "traditional"
            }
          },
          {
            "name": "catchUp",
            "in": "query",
            "required": false,
            "description": "Which catch-up raises the § 402(g) deferral cap, by the age reached during the year: none caps the contribution at $24,500, age50 at $32,500, age60to63 at $35,750 (IRS Notice 2025-67). contribPercent is capped at whichever applies, so contribution can be less than the percent asked for — see contributionLimited. Accepts one of none, age50, age60to63. 6 of the listed spellings are aliases: they are resolved to one of 3 canonical values (`x-canonical-values`) before the answer echoes them. Matching ignores case.",
            "schema": {
              "type": "string",
              "enum": [
                "none",
                "age50",
                "age60to63",
                "50",
                "50+",
                "age50plus",
                "60-63",
                "60to63",
                "age60"
              ],
              "x-canonical-values": [
                "none",
                "age50",
                "age60to63"
              ],
              "default": "none"
            }
          },
          {
            "name": "payPeriods",
            "in": "query",
            "required": false,
            "description": "Paychecks per year: 52 weekly, 26 every two weeks, 24 twice a month, 12 monthly. Accepts integer, a count, between 1 and 365.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 365,
              "default": 26,
              "x-unit": "count",
              "examples": [
                26
              ]
            }
          }
        ]
      },
      "post": {
        "tags": [
          "calculators"
        ],
        "summary": "Take-home pay (federal)",
        "description": "Federal income tax bracket by bracket, Social Security, Medicare and what is left of a salary — per year, per month and per paycheck.\n\nFederal only. No state or local income tax is included, so in 41 states and D.C. real take-home pay is lower than takeHome. /api/v1/state-tax answers the state line where that is a fact rather than a guess: 0 in the 9 states that tax no wages, a computed figure in the 16 with one statutory rate and in California, New Jersey, New York and Virginia, whose published bracket schedules are modelled, and null in the other 22, whose brackets Far Better Off has not verified and will not approximate. Income tax is not the whole of it either: 10 states withhold a disability or paid-leave contribution from the same wages, which the same endpoint answers in \"payrollContributions\" — so takeHome is high by $975 a year for a Californian on $75,000 before a cent of state income tax is counted, and high in Washington, which levies no income tax at all.\n\n2026 figures: brackets and standard deduction from IRS Rev. Proc. 2025-32; Social Security wage base $184,500 and the FICA rates from IRS Topic no. 751; the $24,500 elective-deferral limit and its $8,000 and $11,250 catch-ups from IRS Notice 2025-67.\n\ncontribPercent is capped at the § 402(g) limit, so a percent that would exceed it returns the contribution a plan could actually take and flags contributionLimited. Traditional and Roth deferrals share the one limit; an employer match is outside it and is not modelled here.\n\nThis is the employee half of FICA. It is not self-employment tax, and it does not model W-4 allowances, pre-tax health or HSA premiums, credits, or income other than wages.\n\nIdentical to the GET, with the parameters in a JSON object body.",
        "externalDocs": {
          "description": "The take-home pay (federal) calculator on farbetteroff.com",
          "url": "https://farbetteroff.com/calculators/take-home-paycheck-calculator"
        },
        "responses": {
          "200": {
            "$ref": "#/components/responses/paycheck-result"
          },
          "400": {
            "description": "A parameter is missing, mistyped, out of range or unknown — all of them are listed at once. A name that is one or two edits from a real parameter is answered with the one it probably meant; an ambiguous one is answered with the accepted list instead of a guess.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such endpoint. The response lists every valid slug.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "More than 60 requests in 60 seconds from one IP.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "A bug on our side. The inputs are never at fault for this one.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "operationId": "post-paycheck",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "salary"
                ],
                "properties": {
                  "salary": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 1000000000,
                    "x-unit": "usd",
                    "examples": [
                      75000
                    ],
                    "description": "Gross annual salary. Accepts number, US dollars, between 0 and 1000000000."
                  },
                  "status": {
                    "type": "string",
                    "enum": [
                      "single",
                      "married",
                      "hoh",
                      "mfs"
                    ],
                    "default": "single",
                    "examples": [
                      "single"
                    ],
                    "description": "Filing status: single, married (filing jointly), hoh (head of household) or mfs (married filing separately). Accepts one of single, married, hoh, mfs."
                  },
                  "contribPercent": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 100,
                    "default": 0,
                    "x-unit": "percent",
                    "examples": [
                      5
                    ],
                    "description": "401(k)/403(b) contribution as a percent of salary. See contribType for which side of the tax line it falls on. Accepts number, a percentage, so 6.5 means 6.5%, between 0 and 100."
                  },
                  "contribType": {
                    "type": "string",
                    "enum": [
                      "traditional",
                      "roth",
                      "pretax",
                      "pre-tax",
                      "pre tax",
                      "deferral",
                      "trad",
                      "roth 401k",
                      "roth 401(k)",
                      "designated roth",
                      "after-tax",
                      "aftertax"
                    ],
                    "x-canonical-values": [
                      "traditional",
                      "roth"
                    ],
                    "default": "traditional",
                    "description": "traditional: an elective deferral, exempt from income tax but not from FICA. roth: a designated Roth contribution, which the IRS includes in gross income when it is earned, so it reduces no tax base at all and only takeHome moves. Accepts one of traditional, roth. 10 of the listed spellings are aliases: they are resolved to one of 2 canonical values (`x-canonical-values`) before the answer echoes them. Matching ignores case."
                  },
                  "catchUp": {
                    "type": "string",
                    "enum": [
                      "none",
                      "age50",
                      "age60to63",
                      "50",
                      "50+",
                      "age50plus",
                      "60-63",
                      "60to63",
                      "age60"
                    ],
                    "x-canonical-values": [
                      "none",
                      "age50",
                      "age60to63"
                    ],
                    "default": "none",
                    "description": "Which catch-up raises the § 402(g) deferral cap, by the age reached during the year: none caps the contribution at $24,500, age50 at $32,500, age60to63 at $35,750 (IRS Notice 2025-67). contribPercent is capped at whichever applies, so contribution can be less than the percent asked for — see contributionLimited. Accepts one of none, age50, age60to63. 6 of the listed spellings are aliases: they are resolved to one of 3 canonical values (`x-canonical-values`) before the answer echoes them. Matching ignores case."
                  },
                  "payPeriods": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 365,
                    "default": 26,
                    "x-unit": "count",
                    "examples": [
                      26
                    ],
                    "description": "Paychecks per year: 52 weekly, 26 every two weeks, 24 twice a month, 12 monthly. Accepts integer, a count, between 1 and 365."
                  }
                }
              }
            }
          }
        }
      }
    },
    "/state-tax": {
      "get": {
        "tags": [
          "calculators"
        ],
        "summary": "State income tax on wages",
        "description": "What a state takes out of a salary, for the 29 jurisdictions where that is a verified fact rather than a guess — 9 that tax no wages at all, 16 that charge one statutory rate, and California, New Jersey, New York and Virginia on their own published bracket schedules. For the other 22 it answers null and says why.\n\nWage income only, and 29 of the 51 jurisdictions get a number: 9 that levy no individual income tax on wages, 16 that charge one statutory rate (3 of those after a zero band), and California, New Jersey, New York and Virginia, whose own published bracket schedule is applied in full. For the other 22, \"tax\" is null and \"taxability\" is \"unmodeled\". Far Better Off will not guess at a bracket table it has not read off the state's own forms — so treat null as \"no answer\", never as zero.\n\nWhere a state's own standard deduction has been verified it is applied, by filingStatus, and returned in \"standardDeduction\" with its citation — Arizona, Colorado, Georgia, Idaho, Illinois, Indiana, Iowa, Kentucky, Louisiana, Massachusetts, Michigan, Mississippi, North Carolina and Ohio so far, and in Iowa and Idaho that deduction is the federal one, because both start from the federal figures rather than from your wages. In Illinois it stops above $250,000 ($500,000 filing jointly), and in Ohio it steps down at $40,000 and again at $80,000 of wages and stops above $500,000, as each state's own rule does — so \"standardDeduction\" is what this filer actually gets, not what the statute's headline figure says. Every other flat-rate state's own rule is settled too rather than left out: Pennsylvania grants none, and Utah gives the federal deduction back as a credit (\"credit\") instead. In the graduated states the figure still leaves out personal exemptions and credits, so those filers owe less. County and municipal income taxes are never included, so in Indiana, Kentucky, Michigan, Ohio and much of Pennsylvania the real total is higher. The per-state \"excludes\" field says which of those apply.\n\nPennsylvania is the one state here that taxes 401(k) elective deferrals as compensation, so its 3.07% applies to the whole salary. Everywhere else the rate is applied to wages minus pretaxRetirement.\n\nUtah is the one state here that gives back a credit instead of a deduction, so \"standardDeduction\" is null there and \"ratePercent\" times \"taxedWages\" overstates the bill: 4.5% on every dollar, less a taxpayer tax credit of 6% of the federal standard deduction that shrinks by 1.3¢ for each dollar of income above $18,213 single, $36,426 filing jointly and $27,320 head of household (the 2025 schedule, indexed annually). Two things follow that a flat rate hides — a single Utahn owes nothing below about $20,700, and between there and about $92,500 each extra dollar really meets 5.8%, not 4.5%. The dollars are in \"credit\".\n\nMassachusetts is the one state here with a second rate at the top, so \"ratePercent\" alone does not reproduce \"tax\" there for a very high earner: 5% on wage income after the personal exemption, plus 4 points more on taxable income above $1,107,750 in 2026 (\"surtaxPercent\" and \"surtaxThreshold\", with the dollars in \"surtax\"). The threshold is indexed for inflation each year and is the same whatever your filing status. Below it Massachusetts is flat, and \"surtax\" comes back as 0.\n\n2026 rates, each cited to the state's own revenue department, constitution or code. The citation comes back in the \"source\" field of every answer, and the whole table is readable at /state-income-tax-rates. Michigan's rate, for instance, is re-determined each April and stayed at 4.25% for 2026.\n\nLocal income tax is answered where it has been verified and nowhere else. Pass \"locality\" for one of the rates modelled — Cincinnati 1.8%, Cleveland 2.5%, Columbus 2.5%, Philadelphia 3.735%, Philadelphia 3.425% for a non-resident who works there, Detroit 2.4%, New York City 3.078%–3.876%, Indianapolis (Marion County) 2.02%, Fort Wayne (Allen County) 1.59%, Gary and Hammond (Lake County) 1.5%, Carmel and Fishers (Hamilton County) 1.1%, South Bend (St. Joseph County) 1.75%, Evansville (Vanderburgh County) 1.25%, Lafayette (Tippecanoe County) 1.28%, Bloomington (Monroe County) 2.14%, Louisville 2.2%, Louisville 1.45% for a non-resident who works there, Lexington 2.75%, Lexington 2.25% for a non-resident who works there, Detroit 1.2% for a non-resident who works there, Grand Rapids 1.5%, Grand Rapids 0.75% for a non-resident who works there, Lansing 1%, Lansing 0.5% for a non-resident who works there, Flint 1%, Flint 0.5% for a non-resident who works there, Pontiac 1%, Pontiac 0.5% for a non-resident who works there, East Lansing 1%, East Lansing 0.5% for a non-resident who works there, Battle Creek 1%, Battle Creek 0.5% for a non-resident who works there, Saginaw 1.5%, Saginaw 0.75% for a non-resident who works there, Highland Park 2%, Highland Park 1% for a non-resident who works there, Muskegon 1%, Muskegon 0.5% for a non-resident who works there, Big Rapids 1%, Big Rapids 0.5% for a non-resident who works there, Walker 1%, Walker 0.5% for a non-resident who works there, Hamtramck 1%, Hamtramck 0.5% for a non-resident who works there, Muskegon Heights 1%, Muskegon Heights 0.5% for a non-resident who works there, Portland 1%, Portland 0.5% for a non-resident who works there, Benton Harbor 1%, Benton Harbor 0.5% for a non-resident who works there, Albion 1%, Albion 0.5% for a non-resident who works there, Ionia 1%, Ionia 0.5% for a non-resident who works there, Lapeer 1%, Lapeer 0.5% for a non-resident who works there, Springfield 1%, Springfield 0.5% for a non-resident who works there, Pittsburgh 3%, Pittsburgh 1% for a non-resident who works there, Allentown 1.975%, Allentown 1.28% for a non-resident who works there, Reading 3.6%, Reading 1% for a non-resident who works there, Erie 1.65%, Erie 1.65% for a non-resident who works there, Scranton 3.4%, Scranton 1% for a non-resident who works there, Bethlehem 1%, Bethlehem 1% for a non-resident who works there, Lancaster 1.6%, Lancaster 1% for a non-resident who works there, Harrisburg 2%, Harrisburg 1% for a non-resident who works there, Altoona 1.7%, Altoona 1.4% for a non-resident who works there, York 1.25%, York 1.25% for a non-resident who works there, Wilkes-Barre 3%, Wilkes-Barre 1% for a non-resident who works there, State College Borough 2.25%, State College Borough 1% for a non-resident who works there, Norristown Borough 2.1%, Norristown Borough 1% for a non-resident who works there, West Chester Borough 1.25%, West Chester Borough 1% for a non-resident who works there, Cheltenham Township 1.5%, Cheltenham Township 1% for a non-resident who works there, Upper Darby Township 1%, Bensalem Township 1%, Abington Township 1%, Millcreek Township 1%, Bristol Township 0.5%, Ross Township 1%, Mount Lebanon Township 1.3%, Toledo 2.5%, Akron 2.5%, Dayton 2.5%, Canton 2.5%, Springfield 2.4%, Kettering 2.25%, Parma 2.5%, Lorain 2.5%, Hamilton 2%, Euclid 2.85%, Cuyahoga Falls 2%, Middletown 2%, Mansfield 2.25%, Lakewood 1.5%, St. Louis 1% and Bowling Green 2% — and the answer comes back in \"localTax\", with \"stateAndLocalTax\" for the two lines together. \"localitiesAvailable\" lists what each state offers. They do not share a base, and the answer says which one it used rather than leaving you to assume: Ohio's cities and Philadelphia charge their rate on gross pay with no deduction and no exemption, so a 401(k) contribution does not reduce them (\"localTaxesPretaxRetirement\" is true), while Michigan's cities charge theirs on a federal wage figure the contribution was never in — the AGI on a Detroit resident's return, W-2 box 1 in every other city there — and each allows an exemption of its own ($600 a head in Detroit, Grand Rapids, Lansing, Flint, Pontiac, East Lansing, Highland Park, Muskegon, Big Rapids, Walker, Hamtramck, Muskegon Heights, Albion and Lapeer, and $700 a head in Ionia, and $750 a head in Battle Creek, Saginaw, Benton Harbor and Springfield, and $1000 a head in Portland), so a contribution does reduce all 40 of those rates and \"localExemptions\" is worth passing. New York City is the third base and the only locality here with a schedule rather than a rate: four resident brackets from 3.078% to 3.876% charged on New York State taxable income, so \"localRatePercent\" is null there and \"localBrackets\", \"localMarginalRatePercent\" and \"localTaxableIncome\" are how the answer is reproduced — and \"status\" changes it, because the state's standard deduction and the city's brackets both follow the filing status. Indiana's counties are the fourth base and the first to reuse one: eight of them charge a single rate on the same Indiana taxable income the state taxes, so \"localRatePercent\" and \"localTaxableIncome\" are both filled in and a 401(k) reduces the county line too. St. Louis is the fifth base and the first in a state this API cannot compute: its 1% earnings tax reaches a published list of compensation items — salaries, wages, bonuses, commissions, tips and severance — with deferred compensation and cafeteria plans on the city's own non-taxable list, so a 401(k) reduces it, there is no deduction or exemption to pass, and Missouri's own schedule is not modelled, which is why \"tax\" is null there and \"stateAndLocalTax\" is null with it. What is still missing is missing for the same reason the rest took this long, and Kansas City is the clearest case of it: its Code of Ordinances imposes the same 1% on residents' earnings and on work done in the city, but the RD-109 instructions that would settle whether a 401(k) deferral is inside that figure are served by a host that refuses every automated read, so the rate is absent rather than copied off a page the city does not publish. It will not be guessed at.\n\nOhio asks the local question twice, so this endpoint takes two local parameters and adds both. A municipal tax follows the **work** (\"locality\"); the school district income tax follows the **home** (\"schoolDistrict\"), and all 214 districts that levy one for 2026 are answered — so a Westerville resident working in Columbus owes Columbus's 2.5% on the work and Westerville City School District's 0.75% on the residence, and a request naming only one of them is short by the other. Name a district by its id or by the four-digit code on box 20 of the reader's own W-2, which is the only reliable way to match one: a district's boundaries are not a city's, so a bare name is deliberately not a spelling and \"schoolDistrictsAvailable\" carries the code beside every row. Ohio Revised Code 5748.01(E) lets a district choose one of two bases and they are not interchangeable, which is why \"schoolDistrictBase\" is in every answer: 68 charge their rate on wages with no deduction and no exemption, and 146 charge it on Ohio taxable income — Form SD 100 line 5, the state's personal exemption already off — so 1% of $75,000 is $750 in one and $729 in the other, and $708 filing jointly. Two things that follow surprise people: the state's $26,050 zero band and its $332 of flat dollars stay with the **state**, so $28,400 of wages owes Ohio nothing and owes a 1% district $260.50; and a 401(k) deferral reduces a district line and no Ohio city line, because both district bases are built on federal adjusted gross income and a municipal tax reaches box 5 Medicare wages.\n\nTwo rates come back, and they answer different questions. \"effectiveRatePercent\" is what this year cost; \"marginalRatePercent\" is what the next dollar costs, with the band it came from in \"marginalBracket\" and the whole schedule in \"brackets\". Do not clamp the marginal rate to \"ratePercent\": it is legitimately higher in Massachusetts above the surtax threshold, and in Utah, where a tapering credit takes 1.3 cents of every dollar between about $20,700 and $92,500 of single income, so the real rate on a raise there is 5.8% against an advertised 4.5%. Where a locality is asked for, \"stateAndLocalMarginalRatePercent\" adds the two.\n\nIncome tax is not the only thing a state takes out of a paycheck, and in 10 of them the second line is answered here too: \"payrollContributions\" is the disability, paid-leave and — in New Jersey alone — unemployment contribution withheld from the worker's own wages, with the programmes behind it in \"payrollContributionPrograms\" and the two lines added in \"stateTaxAndContributions\". It behaves like FICA and not like the tax above: charged on gross pay, so a 401(k) deferral does not reduce it, with a per-programme wage base rather than a deduction or a filing status. Washington's two contributions are the clearest case of why this cannot be folded into a rate — the state levies no income tax at all and still takes the most of any state here. Where nothing has been verified the field is null and not zero: Delaware's Paid Leave and Maryland's FAMLI belong in that table and are not in it yet, and New York's disability benefits contribution and Hawaii's TDI are left out because the law lets an employer pay them instead of setting the worker's share.\n\nThis is the state line only unless you ask for a locality. For the federal side — brackets, FICA and take-home pay — call /api/v1/paycheck and subtract this number.",
        "externalDocs": {
          "description": "The state income tax on wages calculator on farbetteroff.com",
          "url": "https://farbetteroff.com/calculators/take-home-paycheck-calculator"
        },
        "responses": {
          "200": {
            "$ref": "#/components/responses/state-tax-result"
          },
          "400": {
            "description": "A parameter is missing, mistyped, out of range or unknown — all of them are listed at once. A name that is one or two edits from a real parameter is answered with the one it probably meant; an ambiguous one is answered with the accepted list instead of a guess.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such endpoint. The response lists every valid slug.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "More than 60 requests in 60 seconds from one IP.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "A bug on our side. The inputs are never at fault for this one.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "operationId": "get-state-tax",
        "parameters": [
          {
            "name": "state",
            "in": "query",
            "required": true,
            "description": "USPS two-letter code, or the full state name (\"Ohio\", \"District of Columbia\"). Where the bare name is taken, the spelling people actually write answers too — \"Washington DC\", \"Washington, D.C.\" and \"D.C.\" for the District, \"Washington State\" and \"New York State\" for the two states whose names those places open. Either case; the code is echoed back. Accepts one of AL, AK, AZ, AR, CA, CO, CT, DE, DC, FL, GA, HI, ID, IL, IN, IA, KS, KY, LA, ME, MD, MA, MI, MN, MS, MO, MT, NE, NV, NH, NJ, NM, NY, NC, ND, OH, OK, OR, PA, RI, SC, SD, TN, TX, UT, VT, VA, WA, WV, WI, WY. 58 of the listed spellings are aliases: they are resolved to one of 51 canonical values (`x-canonical-values`) before the answer echoes them. Matching ignores case.",
            "schema": {
              "type": "string",
              "enum": [
                "AL",
                "AK",
                "AZ",
                "AR",
                "CA",
                "CO",
                "CT",
                "DE",
                "DC",
                "FL",
                "GA",
                "HI",
                "ID",
                "IL",
                "IN",
                "IA",
                "KS",
                "KY",
                "LA",
                "ME",
                "MD",
                "MA",
                "MI",
                "MN",
                "MS",
                "MO",
                "MT",
                "NE",
                "NV",
                "NH",
                "NJ",
                "NM",
                "NY",
                "NC",
                "ND",
                "OH",
                "OK",
                "OR",
                "PA",
                "RI",
                "SC",
                "SD",
                "TN",
                "TX",
                "UT",
                "VT",
                "VA",
                "WA",
                "WV",
                "WI",
                "WY",
                "alabama",
                "alaska",
                "arizona",
                "arkansas",
                "california",
                "colorado",
                "connecticut",
                "delaware",
                "district of columbia",
                "florida",
                "georgia",
                "hawaii",
                "idaho",
                "illinois",
                "indiana",
                "iowa",
                "kansas",
                "kentucky",
                "louisiana",
                "maine",
                "maryland",
                "massachusetts",
                "michigan",
                "minnesota",
                "mississippi",
                "missouri",
                "montana",
                "nebraska",
                "nevada",
                "new hampshire",
                "new jersey",
                "new mexico",
                "new york",
                "north carolina",
                "north dakota",
                "ohio",
                "oklahoma",
                "oregon",
                "pennsylvania",
                "rhode island",
                "south carolina",
                "south dakota",
                "tennessee",
                "texas",
                "utah",
                "vermont",
                "virginia",
                "washington",
                "west virginia",
                "wisconsin",
                "wyoming",
                "washington dc",
                "washington d.c.",
                "washington, dc",
                "washington, d.c.",
                "d.c.",
                "washington state",
                "new york state"
              ],
              "x-canonical-values": [
                "AL",
                "AK",
                "AZ",
                "AR",
                "CA",
                "CO",
                "CT",
                "DE",
                "DC",
                "FL",
                "GA",
                "HI",
                "ID",
                "IL",
                "IN",
                "IA",
                "KS",
                "KY",
                "LA",
                "ME",
                "MD",
                "MA",
                "MI",
                "MN",
                "MS",
                "MO",
                "MT",
                "NE",
                "NV",
                "NH",
                "NJ",
                "NM",
                "NY",
                "NC",
                "ND",
                "OH",
                "OK",
                "OR",
                "PA",
                "RI",
                "SC",
                "SD",
                "TN",
                "TX",
                "UT",
                "VT",
                "VA",
                "WA",
                "WV",
                "WI",
                "WY"
              ],
              "examples": [
                "OH"
              ]
            }
          },
          {
            "name": "wages",
            "in": "query",
            "required": true,
            "description": "Gross annual wages. Accepts number, US dollars, between 0 and 1000000000.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 1000000000,
              "x-unit": "usd",
              "examples": [
                75000
              ]
            }
          },
          {
            "name": "pretaxRetirement",
            "in": "query",
            "required": false,
            "description": "Traditional 401(k)/403(b) dollars contributed over the year. Subtracted from the taxed wages everywhere except Pennsylvania, which taxes elective deferrals. Accepts number, US dollars, between 0 and 1000000000.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 1000000000,
              "default": 0,
              "x-unit": "usd",
              "examples": [
                3750
              ]
            }
          },
          {
            "name": "filingStatus",
            "in": "query",
            "required": false,
            "description": "How this person files, which decides the state's standard deduction where we model one (Georgia exempts $15,000 single and $30,000 married filing jointly). Ignored in states whose deduction is not modelled. Accepts one of single, married, hoh, mfs.",
            "schema": {
              "type": "string",
              "enum": [
                "single",
                "married",
                "hoh",
                "mfs"
              ],
              "default": "single"
            }
          },
          {
            "name": "locality",
            "in": "query",
            "required": false,
            "description": "A local wage tax — a city, county, township or borough — to add on top of the state line, where Far Better Off models one. Omit it and every \"local\" field comes back null, which means not asked rather than none — read \"localitiesAvailable\" for what this state offers. The locality must sit in \"state\"; a mismatch is a 400 rather than a silent swap. Accepts one of oh-cincinnati, oh-cleveland, oh-columbus, pa-philadelphia, pa-philadelphia-nonresident, mi-detroit, ny-new-york-city, in-marion, in-allen, in-lake, in-hamilton, in-st-joseph, in-vanderburgh, in-tippecanoe, in-monroe, ky-louisville, ky-louisville-nonresident, ky-lexington, ky-lexington-nonresident, mi-detroit-nonresident, mi-grand-rapids, mi-grand-rapids-nonresident, mi-lansing, mi-lansing-nonresident, mi-flint, mi-flint-nonresident, mi-pontiac, mi-pontiac-nonresident, mi-east-lansing, mi-east-lansing-nonresident, mi-battle-creek, mi-battle-creek-nonresident, mi-saginaw, mi-saginaw-nonresident, mi-highland-park, mi-highland-park-nonresident, mi-muskegon, mi-muskegon-nonresident, mi-big-rapids, mi-big-rapids-nonresident, mi-walker, mi-walker-nonresident, mi-hamtramck, mi-hamtramck-nonresident, mi-muskegon-heights, mi-muskegon-heights-nonresident, mi-portland, mi-portland-nonresident, mi-benton-harbor, mi-benton-harbor-nonresident, mi-albion, mi-albion-nonresident, mi-ionia, mi-ionia-nonresident, mi-lapeer, mi-lapeer-nonresident, mi-springfield, mi-springfield-nonresident, pa-pittsburgh, pa-pittsburgh-nonresident, pa-allentown, pa-allentown-nonresident, pa-reading, pa-reading-nonresident, pa-erie, pa-erie-nonresident, pa-scranton, pa-scranton-nonresident, pa-bethlehem, pa-bethlehem-nonresident, pa-lancaster, pa-lancaster-nonresident, pa-harrisburg, pa-harrisburg-nonresident, pa-altoona, pa-altoona-nonresident, pa-york, pa-york-nonresident, pa-wilkes-barre, pa-wilkes-barre-nonresident, pa-state-college, pa-state-college-nonresident, pa-norristown, pa-norristown-nonresident, pa-west-chester, pa-west-chester-nonresident, pa-cheltenham, pa-cheltenham-nonresident, pa-upper-darby, pa-bensalem, pa-abington, pa-millcreek, pa-bristol-township, pa-ross-township, pa-mount-lebanon, oh-toledo, oh-akron, oh-dayton, oh-canton, oh-springfield, oh-kettering, oh-parma, oh-lorain, oh-hamilton, oh-euclid, oh-cuyahoga-falls, oh-middletown, oh-mansfield, oh-lakewood, mo-st-louis, ky-bowling-green. 83 of the listed spellings are aliases: they are resolved to one of 111 canonical values (`x-canonical-values`) before the answer echoes them. Matching ignores case.",
            "schema": {
              "type": "string",
              "enum": [
                "oh-cincinnati",
                "oh-cleveland",
                "oh-columbus",
                "pa-philadelphia",
                "pa-philadelphia-nonresident",
                "mi-detroit",
                "ny-new-york-city",
                "in-marion",
                "in-allen",
                "in-lake",
                "in-hamilton",
                "in-st-joseph",
                "in-vanderburgh",
                "in-tippecanoe",
                "in-monroe",
                "ky-louisville",
                "ky-louisville-nonresident",
                "ky-lexington",
                "ky-lexington-nonresident",
                "mi-detroit-nonresident",
                "mi-grand-rapids",
                "mi-grand-rapids-nonresident",
                "mi-lansing",
                "mi-lansing-nonresident",
                "mi-flint",
                "mi-flint-nonresident",
                "mi-pontiac",
                "mi-pontiac-nonresident",
                "mi-east-lansing",
                "mi-east-lansing-nonresident",
                "mi-battle-creek",
                "mi-battle-creek-nonresident",
                "mi-saginaw",
                "mi-saginaw-nonresident",
                "mi-highland-park",
                "mi-highland-park-nonresident",
                "mi-muskegon",
                "mi-muskegon-nonresident",
                "mi-big-rapids",
                "mi-big-rapids-nonresident",
                "mi-walker",
                "mi-walker-nonresident",
                "mi-hamtramck",
                "mi-hamtramck-nonresident",
                "mi-muskegon-heights",
                "mi-muskegon-heights-nonresident",
                "mi-portland",
                "mi-portland-nonresident",
                "mi-benton-harbor",
                "mi-benton-harbor-nonresident",
                "mi-albion",
                "mi-albion-nonresident",
                "mi-ionia",
                "mi-ionia-nonresident",
                "mi-lapeer",
                "mi-lapeer-nonresident",
                "mi-springfield",
                "mi-springfield-nonresident",
                "pa-pittsburgh",
                "pa-pittsburgh-nonresident",
                "pa-allentown",
                "pa-allentown-nonresident",
                "pa-reading",
                "pa-reading-nonresident",
                "pa-erie",
                "pa-erie-nonresident",
                "pa-scranton",
                "pa-scranton-nonresident",
                "pa-bethlehem",
                "pa-bethlehem-nonresident",
                "pa-lancaster",
                "pa-lancaster-nonresident",
                "pa-harrisburg",
                "pa-harrisburg-nonresident",
                "pa-altoona",
                "pa-altoona-nonresident",
                "pa-york",
                "pa-york-nonresident",
                "pa-wilkes-barre",
                "pa-wilkes-barre-nonresident",
                "pa-state-college",
                "pa-state-college-nonresident",
                "pa-norristown",
                "pa-norristown-nonresident",
                "pa-west-chester",
                "pa-west-chester-nonresident",
                "pa-cheltenham",
                "pa-cheltenham-nonresident",
                "pa-upper-darby",
                "pa-bensalem",
                "pa-abington",
                "pa-millcreek",
                "pa-bristol-township",
                "pa-ross-township",
                "pa-mount-lebanon",
                "oh-toledo",
                "oh-akron",
                "oh-dayton",
                "oh-canton",
                "oh-springfield",
                "oh-kettering",
                "oh-parma",
                "oh-lorain",
                "oh-hamilton",
                "oh-euclid",
                "oh-cuyahoga-falls",
                "oh-middletown",
                "oh-mansfield",
                "oh-lakewood",
                "mo-st-louis",
                "ky-bowling-green",
                "pittsburgh",
                "allentown",
                "reading",
                "erie",
                "scranton",
                "bethlehem",
                "lancaster",
                "harrisburg",
                "altoona",
                "york",
                "wilkes-barre",
                "state-college",
                "norristown",
                "west-chester",
                "cheltenham",
                "upper-darby",
                "bensalem",
                "abington",
                "millcreek",
                "ross-township",
                "mount-lebanon",
                "mt-lebanon",
                "bristol-township",
                "cincinnati",
                "cleveland",
                "columbus",
                "toledo",
                "akron",
                "dayton",
                "canton",
                "kettering",
                "parma",
                "lorain",
                "euclid",
                "cuyahoga-falls",
                "middletown",
                "mansfield",
                "lakewood",
                "hamilton-oh",
                "hamilton-in",
                "detroit",
                "grand-rapids",
                "lansing",
                "flint",
                "pontiac",
                "east-lansing",
                "battle-creek",
                "saginaw",
                "highland-park",
                "muskegon",
                "big-rapids",
                "walker",
                "hamtramck",
                "muskegon-heights",
                "portland",
                "portland-mi",
                "benton-harbor",
                "albion",
                "ionia",
                "lapeer",
                "springfield-mi",
                "springfield-oh",
                "philadelphia",
                "nyc",
                "new-york-city",
                "indianapolis",
                "fort-wayne",
                "gary",
                "hammond",
                "carmel",
                "fishers",
                "south-bend",
                "evansville",
                "lafayette",
                "bloomington",
                "louisville",
                "lexington",
                "bowling-green",
                "lexington-fayette",
                "st-louis",
                "st. louis",
                "saint-louis",
                "st-louis-city"
              ],
              "x-canonical-values": [
                "oh-cincinnati",
                "oh-cleveland",
                "oh-columbus",
                "pa-philadelphia",
                "pa-philadelphia-nonresident",
                "mi-detroit",
                "ny-new-york-city",
                "in-marion",
                "in-allen",
                "in-lake",
                "in-hamilton",
                "in-st-joseph",
                "in-vanderburgh",
                "in-tippecanoe",
                "in-monroe",
                "ky-louisville",
                "ky-louisville-nonresident",
                "ky-lexington",
                "ky-lexington-nonresident",
                "mi-detroit-nonresident",
                "mi-grand-rapids",
                "mi-grand-rapids-nonresident",
                "mi-lansing",
                "mi-lansing-nonresident",
                "mi-flint",
                "mi-flint-nonresident",
                "mi-pontiac",
                "mi-pontiac-nonresident",
                "mi-east-lansing",
                "mi-east-lansing-nonresident",
                "mi-battle-creek",
                "mi-battle-creek-nonresident",
                "mi-saginaw",
                "mi-saginaw-nonresident",
                "mi-highland-park",
                "mi-highland-park-nonresident",
                "mi-muskegon",
                "mi-muskegon-nonresident",
                "mi-big-rapids",
                "mi-big-rapids-nonresident",
                "mi-walker",
                "mi-walker-nonresident",
                "mi-hamtramck",
                "mi-hamtramck-nonresident",
                "mi-muskegon-heights",
                "mi-muskegon-heights-nonresident",
                "mi-portland",
                "mi-portland-nonresident",
                "mi-benton-harbor",
                "mi-benton-harbor-nonresident",
                "mi-albion",
                "mi-albion-nonresident",
                "mi-ionia",
                "mi-ionia-nonresident",
                "mi-lapeer",
                "mi-lapeer-nonresident",
                "mi-springfield",
                "mi-springfield-nonresident",
                "pa-pittsburgh",
                "pa-pittsburgh-nonresident",
                "pa-allentown",
                "pa-allentown-nonresident",
                "pa-reading",
                "pa-reading-nonresident",
                "pa-erie",
                "pa-erie-nonresident",
                "pa-scranton",
                "pa-scranton-nonresident",
                "pa-bethlehem",
                "pa-bethlehem-nonresident",
                "pa-lancaster",
                "pa-lancaster-nonresident",
                "pa-harrisburg",
                "pa-harrisburg-nonresident",
                "pa-altoona",
                "pa-altoona-nonresident",
                "pa-york",
                "pa-york-nonresident",
                "pa-wilkes-barre",
                "pa-wilkes-barre-nonresident",
                "pa-state-college",
                "pa-state-college-nonresident",
                "pa-norristown",
                "pa-norristown-nonresident",
                "pa-west-chester",
                "pa-west-chester-nonresident",
                "pa-cheltenham",
                "pa-cheltenham-nonresident",
                "pa-upper-darby",
                "pa-bensalem",
                "pa-abington",
                "pa-millcreek",
                "pa-bristol-township",
                "pa-ross-township",
                "pa-mount-lebanon",
                "oh-toledo",
                "oh-akron",
                "oh-dayton",
                "oh-canton",
                "oh-springfield",
                "oh-kettering",
                "oh-parma",
                "oh-lorain",
                "oh-hamilton",
                "oh-euclid",
                "oh-cuyahoga-falls",
                "oh-middletown",
                "oh-mansfield",
                "oh-lakewood",
                "mo-st-louis",
                "ky-bowling-green"
              ],
              "examples": [
                "oh-columbus"
              ]
            }
          },
          {
            "name": "schoolDistrict",
            "in": "query",
            "required": false,
            "description": "Ohio's school district income tax, which is a second local levy on the same paycheck and the only one here charged by where you **live** rather than where you work — so it is owed on top of \"locality\" and not instead of it. All 214 districts that levy one for 2026 are accepted, by id or by the four-digit code from box 20 of your W-2 (\"2301\"), which is the only reliable way to match one: a district's boundaries are not a city's, a township's or a ZIP code's, so a bare name is deliberately not a spelling. Omit it and every \"schoolDistrict\" field comes back null, which means not asked rather than none — read \"schoolDistrictsAvailable\" for what this state offers. The district must sit in \"state\"; a mismatch is a 400 rather than a silent swap. Accepts one of 214 Ohio school district ids (oh-sd-2301-amanda-clearcreek), or the four-digit district code from box 20 of your W-2 (2301) — the list is in \"schoolDistrictsAvailable\" on any answer for state=OH. 214 of the listed spellings are aliases: they are resolved to one of 214 canonical values (`x-canonical-values`) before the answer echoes them. Matching ignores case.",
            "schema": {
              "type": "string",
              "enum": [
                "oh-sd-2301-amanda-clearcreek",
                "oh-sd-0502-athens",
                "oh-sd-2801-berkshire",
                "oh-sd-2302-berne-union",
                "oh-sd-5501-bethel",
                "oh-sd-1401-blanchester",
                "oh-sd-7502-botkins",
                "oh-sd-5901-cardington-lincoln",
                "oh-sd-5401-celina",
                "oh-sd-8501-chippewa",
                "oh-sd-6501-circleville",
                "oh-sd-7001-clear-fork-valley",
                "oh-sd-1402-clinton-massie",
                "oh-sd-5204-cloverleaf",
                "oh-sd-7201-clyde-green-springs",
                "oh-sd-1704-crestline",
                "oh-sd-8702-eastwood",
                "oh-sd-5101-elgin",
                "oh-sd-3204-findlay",
                "oh-sd-0404-geneva-area",
                "oh-sd-7203-gibsonburg",
                "oh-sd-8503-green",
                "oh-sd-3603-greenfield",
                "oh-sd-0302-hillsdale",
                "oh-sd-7403-hopewell-loudon",
                "oh-sd-7506-jackson-center",
                "oh-sd-6704-james-a-garfield",
                "oh-sd-4901-jefferson",
                "oh-sd-4902-jonathan-alder",
                "oh-sd-8303-kings",
                "oh-sd-2305-lancaster",
                "oh-sd-6502-logan-elm",
                "oh-sd-4904-madison-plains",
                "oh-sd-5403-marion",
                "oh-sd-5504-miami-east",
                "oh-sd-5505-milton-union",
                "oh-sd-3902-monroeville",
                "oh-sd-8605-montpelier",
                "oh-sd-8705-north-baltimore",
                "oh-sd-4508-north-fork",
                "oh-sd-1203-northeastern",
                "oh-sd-4509-northridge",
                "oh-sd-7612-northwest",
                "oh-sd-1204-northwestern",
                "oh-sd-8706-northwood",
                "oh-sd-7711-norton",
                "oh-sd-8504-norwayne",
                "oh-sd-5103-pleasant",
                "oh-sd-5104-ridgedale",
                "oh-sd-5105-river-valley",
                "oh-sd-4604-riverside",
                "oh-sd-0908-ross",
                "oh-sd-5008-sebring",
                "oh-sd-7508-sidney",
                "oh-sd-3118-southwest",
                "oh-sd-0604-st-marys",
                "oh-sd-6503-teays-valley",
                "oh-sd-7407-tiffin",
                "oh-sd-6806-tri-county-north",
                "oh-sd-0505-trimble",
                "oh-sd-8509-triway",
                "oh-sd-5509-troy",
                "oh-sd-2308-walnut-township",
                "oh-sd-2402-washington-court-house",
                "oh-sd-2607-wauseon",
                "oh-sd-2514-westerville",
                "oh-sd-3907-willard",
                "oh-sd-7107-zane-trace",
                "oh-sd-3301-ada",
                "oh-sd-7501-anna",
                "oh-sd-1901-ansonia",
                "oh-sd-6301-antwerp",
                "oh-sd-3201-arcadia",
                "oh-sd-1902-arcanum-butler",
                "oh-sd-3202-arlington",
                "oh-sd-2001-ayersville",
                "oh-sd-3901-bellevue",
                "oh-sd-2501-bexley",
                "oh-sd-2101-big-walnut",
                "oh-sd-2303-bloom-carroll",
                "oh-sd-0203-bluffton",
                "oh-sd-8701-bowling-green",
                "oh-sd-5502-bradford",
                "oh-sd-8601-bryan",
                "oh-sd-1701-buckeye-central",
                "oh-sd-2102-buckeye-valley",
                "oh-sd-2502-canal-winchester",
                "oh-sd-8801-carey",
                "oh-sd-8301-carlisle",
                "oh-sd-2902-cedar-cliff",
                "oh-sd-4201-centerburg",
                "oh-sd-2002-central",
                "oh-sd-1303-clermont-northeastern",
                "oh-sd-5402-coldwater",
                "oh-sd-1703-colonel-crawford",
                "oh-sd-1502-columbiana",
                "oh-sd-6901-columbus-grove",
                "oh-sd-6902-continental",
                "oh-sd-3203-cory-rawson",
                "oh-sd-5503-covington",
                "oh-sd-1503-crestview",
                "oh-sd-8101-crestview",
                "oh-sd-8502-dalton",
                "oh-sd-4202-danville",
                "oh-sd-2003-defiance",
                "oh-sd-0204-delphos",
                "oh-sd-6803-eaton",
                "oh-sd-8602-edgerton",
                "oh-sd-8703-elmwood",
                "oh-sd-2602-evergreen",
                "oh-sd-8001-fairbanks",
                "oh-sd-2903-fairborn",
                "oh-sd-2304-fairfield-union",
                "oh-sd-7503-fairlawn",
                "oh-sd-2603-fayette",
                "oh-sd-7504-fort-loramie",
                "oh-sd-5406-fort-recovery",
                "oh-sd-1903-franklin-monroe",
                "oh-sd-7202-fremont",
                "oh-sd-1305-goshen",
                "oh-sd-4501-granville",
                "oh-sd-2904-greeneview",
                "oh-sd-1904-greenville",
                "oh-sd-3302-hardin-northern",
                "oh-sd-7505-hardin-houston",
                "oh-sd-2004-hicksville",
                "oh-sd-5902-highland",
                "oh-sd-3604-hillsboro",
                "oh-sd-3501-holgate",
                "oh-sd-6903-jennings",
                "oh-sd-4503-johnstown-monroe",
                "oh-sd-6904-kalida",
                "oh-sd-3303-kenton",
                "oh-sd-7204-lakota",
                "oh-sd-6905-leipsic",
                "oh-sd-3502-liberty-center",
                "oh-sd-2306-liberty-union-thurston",
                "oh-sd-3205-liberty-benton",
                "oh-sd-4506-licking-valley",
                "oh-sd-4903-london",
                "oh-sd-0303-loudonville-perrysville",
                "oh-sd-0905-madison",
                "oh-sd-3206-mccomb",
                "oh-sd-1102-mechanicsburg",
                "oh-sd-8604-millcreek-west-unity",
                "oh-sd-6906-miller-city-new-cleveland",
                "oh-sd-0601-minster",
                "oh-sd-1905-mississinawa-valley",
                "oh-sd-8802-mohawk",
                "oh-sd-5903-mount-gilead",
                "oh-sd-6802-national-trail",
                "oh-sd-0602-new-bremen",
                "oh-sd-0603-new-knoxville",
                "oh-sd-5708-new-lebanon",
                "oh-sd-3903-new-london",
                "oh-sd-0907-new-miami",
                "oh-sd-7404-new-riegel",
                "oh-sd-4507-newark",
                "oh-sd-5506-newton",
                "oh-sd-8003-north-union",
                "oh-sd-5904-northmor",
                "oh-sd-8505-northwestern",
                "oh-sd-3904-norwalk",
                "oh-sd-4712-oberlin",
                "oh-sd-7405-old-fort",
                "oh-sd-8707-otsego",
                "oh-sd-6907-ottawa-glandorf",
                "oh-sd-6908-ottoville",
                "oh-sd-6909-pandora-gilboa",
                "oh-sd-5405-parkway",
                "oh-sd-3504-patrick-henry",
                "oh-sd-6302-paulding",
                "oh-sd-8708-perrysburg",
                "oh-sd-2604-pettisville",
                "oh-sd-2307-pickerington",
                "oh-sd-2605-pike-delta-york",
                "oh-sd-5507-piqua",
                "oh-sd-7007-plymouth-shiloh",
                "oh-sd-6804-preble-shawnee",
                "oh-sd-2509-reynoldsburg",
                "oh-sd-3304-ridgemont",
                "oh-sd-3305-riverdale",
                "oh-sd-7507-russia",
                "oh-sd-7406-seneca-east",
                "oh-sd-7008-shelby",
                "oh-sd-3905-south-central",
                "oh-sd-1205-southeastern",
                "oh-sd-4510-southwest-licking",
                "oh-sd-0209-spencerville",
                "oh-sd-5010-springfield",
                "oh-sd-8607-stryker",
                "oh-sd-2606-swanton",
                "oh-sd-0909-talawanda",
                "oh-sd-1906-tri-village",
                "oh-sd-1103-triad",
                "oh-sd-6805-twin-valley-community",
                "oh-sd-7106-union-scioto",
                "oh-sd-1510-united",
                "oh-sd-8803-upper-sandusky",
                "oh-sd-3306-upper-scioto-valley",
                "oh-sd-5713-valley-view",
                "oh-sd-3207-van-buren",
                "oh-sd-8104-van-wert",
                "oh-sd-3208-vanlue",
                "oh-sd-1907-versailles",
                "oh-sd-0605-wapakoneta",
                "oh-sd-6303-wayne-trace",
                "oh-sd-0606-waynesfield-goshen",
                "oh-sd-4715-wellington",
                "oh-sd-1105-west-liberty-salem",
                "oh-sd-3906-western-reserve",
                "oh-sd-3122-wyoming",
                "oh-sd-2906-xenia-community",
                "oh-sd-2907-yellow-springs",
                "1102",
                "1103",
                "1105",
                "1203",
                "1204",
                "1205",
                "1303",
                "1305",
                "1401",
                "1402",
                "1502",
                "1503",
                "1510",
                "1701",
                "1703",
                "1704",
                "1901",
                "1902",
                "1903",
                "1904",
                "1905",
                "1906",
                "1907",
                "2001",
                "2002",
                "2003",
                "2004",
                "2101",
                "2102",
                "2301",
                "2302",
                "2303",
                "2304",
                "2305",
                "2306",
                "2307",
                "2308",
                "2402",
                "2501",
                "2502",
                "2509",
                "2514",
                "2602",
                "2603",
                "2604",
                "2605",
                "2606",
                "2607",
                "2801",
                "2902",
                "2903",
                "2904",
                "2906",
                "2907",
                "3118",
                "3122",
                "3201",
                "3202",
                "3203",
                "3204",
                "3205",
                "3206",
                "3207",
                "3208",
                "3301",
                "3302",
                "3303",
                "3304",
                "3305",
                "3306",
                "3501",
                "3502",
                "3504",
                "3603",
                "3604",
                "3901",
                "3902",
                "3903",
                "3904",
                "3905",
                "3906",
                "3907",
                "4201",
                "4202",
                "4501",
                "4503",
                "4506",
                "4507",
                "4508",
                "4509",
                "4510",
                "4604",
                "4712",
                "4715",
                "4901",
                "4902",
                "4903",
                "4904",
                "5008",
                "5010",
                "5101",
                "5103",
                "5104",
                "5105",
                "5204",
                "5401",
                "5402",
                "5403",
                "5405",
                "5406",
                "5501",
                "5502",
                "5503",
                "5504",
                "5505",
                "5506",
                "5507",
                "5509",
                "5708",
                "5713",
                "5901",
                "5902",
                "5903",
                "5904",
                "6301",
                "6302",
                "6303",
                "6501",
                "6502",
                "6503",
                "6704",
                "6802",
                "6803",
                "6804",
                "6805",
                "6806",
                "6901",
                "6902",
                "6903",
                "6904",
                "6905",
                "6906",
                "6907",
                "6908",
                "6909",
                "7001",
                "7007",
                "7008",
                "7106",
                "7107",
                "7201",
                "7202",
                "7203",
                "7204",
                "7403",
                "7404",
                "7405",
                "7406",
                "7407",
                "7501",
                "7502",
                "7503",
                "7504",
                "7505",
                "7506",
                "7507",
                "7508",
                "7612",
                "7711",
                "8001",
                "8003",
                "8101",
                "8104",
                "8301",
                "8303",
                "8501",
                "8502",
                "8503",
                "8504",
                "8505",
                "8509",
                "8601",
                "8602",
                "8604",
                "8605",
                "8607",
                "8701",
                "8702",
                "8703",
                "8705",
                "8706",
                "8707",
                "8708",
                "8801",
                "8802",
                "8803",
                "0502",
                "0404",
                "0302",
                "0908",
                "0604",
                "0505",
                "0203",
                "0204",
                "0303",
                "0905",
                "0601",
                "0602",
                "0603",
                "0907",
                "0209",
                "0909",
                "0605",
                "0606"
              ],
              "x-canonical-values": [
                "oh-sd-2301-amanda-clearcreek",
                "oh-sd-0502-athens",
                "oh-sd-2801-berkshire",
                "oh-sd-2302-berne-union",
                "oh-sd-5501-bethel",
                "oh-sd-1401-blanchester",
                "oh-sd-7502-botkins",
                "oh-sd-5901-cardington-lincoln",
                "oh-sd-5401-celina",
                "oh-sd-8501-chippewa",
                "oh-sd-6501-circleville",
                "oh-sd-7001-clear-fork-valley",
                "oh-sd-1402-clinton-massie",
                "oh-sd-5204-cloverleaf",
                "oh-sd-7201-clyde-green-springs",
                "oh-sd-1704-crestline",
                "oh-sd-8702-eastwood",
                "oh-sd-5101-elgin",
                "oh-sd-3204-findlay",
                "oh-sd-0404-geneva-area",
                "oh-sd-7203-gibsonburg",
                "oh-sd-8503-green",
                "oh-sd-3603-greenfield",
                "oh-sd-0302-hillsdale",
                "oh-sd-7403-hopewell-loudon",
                "oh-sd-7506-jackson-center",
                "oh-sd-6704-james-a-garfield",
                "oh-sd-4901-jefferson",
                "oh-sd-4902-jonathan-alder",
                "oh-sd-8303-kings",
                "oh-sd-2305-lancaster",
                "oh-sd-6502-logan-elm",
                "oh-sd-4904-madison-plains",
                "oh-sd-5403-marion",
                "oh-sd-5504-miami-east",
                "oh-sd-5505-milton-union",
                "oh-sd-3902-monroeville",
                "oh-sd-8605-montpelier",
                "oh-sd-8705-north-baltimore",
                "oh-sd-4508-north-fork",
                "oh-sd-1203-northeastern",
                "oh-sd-4509-northridge",
                "oh-sd-7612-northwest",
                "oh-sd-1204-northwestern",
                "oh-sd-8706-northwood",
                "oh-sd-7711-norton",
                "oh-sd-8504-norwayne",
                "oh-sd-5103-pleasant",
                "oh-sd-5104-ridgedale",
                "oh-sd-5105-river-valley",
                "oh-sd-4604-riverside",
                "oh-sd-0908-ross",
                "oh-sd-5008-sebring",
                "oh-sd-7508-sidney",
                "oh-sd-3118-southwest",
                "oh-sd-0604-st-marys",
                "oh-sd-6503-teays-valley",
                "oh-sd-7407-tiffin",
                "oh-sd-6806-tri-county-north",
                "oh-sd-0505-trimble",
                "oh-sd-8509-triway",
                "oh-sd-5509-troy",
                "oh-sd-2308-walnut-township",
                "oh-sd-2402-washington-court-house",
                "oh-sd-2607-wauseon",
                "oh-sd-2514-westerville",
                "oh-sd-3907-willard",
                "oh-sd-7107-zane-trace",
                "oh-sd-3301-ada",
                "oh-sd-7501-anna",
                "oh-sd-1901-ansonia",
                "oh-sd-6301-antwerp",
                "oh-sd-3201-arcadia",
                "oh-sd-1902-arcanum-butler",
                "oh-sd-3202-arlington",
                "oh-sd-2001-ayersville",
                "oh-sd-3901-bellevue",
                "oh-sd-2501-bexley",
                "oh-sd-2101-big-walnut",
                "oh-sd-2303-bloom-carroll",
                "oh-sd-0203-bluffton",
                "oh-sd-8701-bowling-green",
                "oh-sd-5502-bradford",
                "oh-sd-8601-bryan",
                "oh-sd-1701-buckeye-central",
                "oh-sd-2102-buckeye-valley",
                "oh-sd-2502-canal-winchester",
                "oh-sd-8801-carey",
                "oh-sd-8301-carlisle",
                "oh-sd-2902-cedar-cliff",
                "oh-sd-4201-centerburg",
                "oh-sd-2002-central",
                "oh-sd-1303-clermont-northeastern",
                "oh-sd-5402-coldwater",
                "oh-sd-1703-colonel-crawford",
                "oh-sd-1502-columbiana",
                "oh-sd-6901-columbus-grove",
                "oh-sd-6902-continental",
                "oh-sd-3203-cory-rawson",
                "oh-sd-5503-covington",
                "oh-sd-1503-crestview",
                "oh-sd-8101-crestview",
                "oh-sd-8502-dalton",
                "oh-sd-4202-danville",
                "oh-sd-2003-defiance",
                "oh-sd-0204-delphos",
                "oh-sd-6803-eaton",
                "oh-sd-8602-edgerton",
                "oh-sd-8703-elmwood",
                "oh-sd-2602-evergreen",
                "oh-sd-8001-fairbanks",
                "oh-sd-2903-fairborn",
                "oh-sd-2304-fairfield-union",
                "oh-sd-7503-fairlawn",
                "oh-sd-2603-fayette",
                "oh-sd-7504-fort-loramie",
                "oh-sd-5406-fort-recovery",
                "oh-sd-1903-franklin-monroe",
                "oh-sd-7202-fremont",
                "oh-sd-1305-goshen",
                "oh-sd-4501-granville",
                "oh-sd-2904-greeneview",
                "oh-sd-1904-greenville",
                "oh-sd-3302-hardin-northern",
                "oh-sd-7505-hardin-houston",
                "oh-sd-2004-hicksville",
                "oh-sd-5902-highland",
                "oh-sd-3604-hillsboro",
                "oh-sd-3501-holgate",
                "oh-sd-6903-jennings",
                "oh-sd-4503-johnstown-monroe",
                "oh-sd-6904-kalida",
                "oh-sd-3303-kenton",
                "oh-sd-7204-lakota",
                "oh-sd-6905-leipsic",
                "oh-sd-3502-liberty-center",
                "oh-sd-2306-liberty-union-thurston",
                "oh-sd-3205-liberty-benton",
                "oh-sd-4506-licking-valley",
                "oh-sd-4903-london",
                "oh-sd-0303-loudonville-perrysville",
                "oh-sd-0905-madison",
                "oh-sd-3206-mccomb",
                "oh-sd-1102-mechanicsburg",
                "oh-sd-8604-millcreek-west-unity",
                "oh-sd-6906-miller-city-new-cleveland",
                "oh-sd-0601-minster",
                "oh-sd-1905-mississinawa-valley",
                "oh-sd-8802-mohawk",
                "oh-sd-5903-mount-gilead",
                "oh-sd-6802-national-trail",
                "oh-sd-0602-new-bremen",
                "oh-sd-0603-new-knoxville",
                "oh-sd-5708-new-lebanon",
                "oh-sd-3903-new-london",
                "oh-sd-0907-new-miami",
                "oh-sd-7404-new-riegel",
                "oh-sd-4507-newark",
                "oh-sd-5506-newton",
                "oh-sd-8003-north-union",
                "oh-sd-5904-northmor",
                "oh-sd-8505-northwestern",
                "oh-sd-3904-norwalk",
                "oh-sd-4712-oberlin",
                "oh-sd-7405-old-fort",
                "oh-sd-8707-otsego",
                "oh-sd-6907-ottawa-glandorf",
                "oh-sd-6908-ottoville",
                "oh-sd-6909-pandora-gilboa",
                "oh-sd-5405-parkway",
                "oh-sd-3504-patrick-henry",
                "oh-sd-6302-paulding",
                "oh-sd-8708-perrysburg",
                "oh-sd-2604-pettisville",
                "oh-sd-2307-pickerington",
                "oh-sd-2605-pike-delta-york",
                "oh-sd-5507-piqua",
                "oh-sd-7007-plymouth-shiloh",
                "oh-sd-6804-preble-shawnee",
                "oh-sd-2509-reynoldsburg",
                "oh-sd-3304-ridgemont",
                "oh-sd-3305-riverdale",
                "oh-sd-7507-russia",
                "oh-sd-7406-seneca-east",
                "oh-sd-7008-shelby",
                "oh-sd-3905-south-central",
                "oh-sd-1205-southeastern",
                "oh-sd-4510-southwest-licking",
                "oh-sd-0209-spencerville",
                "oh-sd-5010-springfield",
                "oh-sd-8607-stryker",
                "oh-sd-2606-swanton",
                "oh-sd-0909-talawanda",
                "oh-sd-1906-tri-village",
                "oh-sd-1103-triad",
                "oh-sd-6805-twin-valley-community",
                "oh-sd-7106-union-scioto",
                "oh-sd-1510-united",
                "oh-sd-8803-upper-sandusky",
                "oh-sd-3306-upper-scioto-valley",
                "oh-sd-5713-valley-view",
                "oh-sd-3207-van-buren",
                "oh-sd-8104-van-wert",
                "oh-sd-3208-vanlue",
                "oh-sd-1907-versailles",
                "oh-sd-0605-wapakoneta",
                "oh-sd-6303-wayne-trace",
                "oh-sd-0606-waynesfield-goshen",
                "oh-sd-4715-wellington",
                "oh-sd-1105-west-liberty-salem",
                "oh-sd-3906-western-reserve",
                "oh-sd-3122-wyoming",
                "oh-sd-2906-xenia-community",
                "oh-sd-2907-yellow-springs"
              ],
              "examples": [
                "2514"
              ]
            }
          },
          {
            "name": "localExemptions",
            "in": "query",
            "required": false,
            "description": "Personal and dependency exemptions to claim against the locality's tax, as a head count — the filer, a spouse, each dependent. Only Michigan's 40 rates allow any of the localities modelled ($600 a head in Detroit, Grand Rapids, Lansing, Flint, Pontiac, East Lansing, Highland Park, Muskegon, Big Rapids, Walker, Hamtramck, Muskegon Heights, Albion and Lapeer, and $700 a head in Ionia, and $750 a head in Battle Creek, Saginaw, Benton Harbor and Springfield, and $1000 a head in Portland, read back in \"localExemptionAmount\"); elsewhere it changes nothing. The default of 0 is the larger bill, so an omitted count never understates a city. Accepts number, between 0 and 20.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 20,
              "default": 0
            }
          }
        ]
      },
      "post": {
        "tags": [
          "calculators"
        ],
        "summary": "State income tax on wages",
        "description": "What a state takes out of a salary, for the 29 jurisdictions where that is a verified fact rather than a guess — 9 that tax no wages at all, 16 that charge one statutory rate, and California, New Jersey, New York and Virginia on their own published bracket schedules. For the other 22 it answers null and says why.\n\nWage income only, and 29 of the 51 jurisdictions get a number: 9 that levy no individual income tax on wages, 16 that charge one statutory rate (3 of those after a zero band), and California, New Jersey, New York and Virginia, whose own published bracket schedule is applied in full. For the other 22, \"tax\" is null and \"taxability\" is \"unmodeled\". Far Better Off will not guess at a bracket table it has not read off the state's own forms — so treat null as \"no answer\", never as zero.\n\nWhere a state's own standard deduction has been verified it is applied, by filingStatus, and returned in \"standardDeduction\" with its citation — Arizona, Colorado, Georgia, Idaho, Illinois, Indiana, Iowa, Kentucky, Louisiana, Massachusetts, Michigan, Mississippi, North Carolina and Ohio so far, and in Iowa and Idaho that deduction is the federal one, because both start from the federal figures rather than from your wages. In Illinois it stops above $250,000 ($500,000 filing jointly), and in Ohio it steps down at $40,000 and again at $80,000 of wages and stops above $500,000, as each state's own rule does — so \"standardDeduction\" is what this filer actually gets, not what the statute's headline figure says. Every other flat-rate state's own rule is settled too rather than left out: Pennsylvania grants none, and Utah gives the federal deduction back as a credit (\"credit\") instead. In the graduated states the figure still leaves out personal exemptions and credits, so those filers owe less. County and municipal income taxes are never included, so in Indiana, Kentucky, Michigan, Ohio and much of Pennsylvania the real total is higher. The per-state \"excludes\" field says which of those apply.\n\nPennsylvania is the one state here that taxes 401(k) elective deferrals as compensation, so its 3.07% applies to the whole salary. Everywhere else the rate is applied to wages minus pretaxRetirement.\n\nUtah is the one state here that gives back a credit instead of a deduction, so \"standardDeduction\" is null there and \"ratePercent\" times \"taxedWages\" overstates the bill: 4.5% on every dollar, less a taxpayer tax credit of 6% of the federal standard deduction that shrinks by 1.3¢ for each dollar of income above $18,213 single, $36,426 filing jointly and $27,320 head of household (the 2025 schedule, indexed annually). Two things follow that a flat rate hides — a single Utahn owes nothing below about $20,700, and between there and about $92,500 each extra dollar really meets 5.8%, not 4.5%. The dollars are in \"credit\".\n\nMassachusetts is the one state here with a second rate at the top, so \"ratePercent\" alone does not reproduce \"tax\" there for a very high earner: 5% on wage income after the personal exemption, plus 4 points more on taxable income above $1,107,750 in 2026 (\"surtaxPercent\" and \"surtaxThreshold\", with the dollars in \"surtax\"). The threshold is indexed for inflation each year and is the same whatever your filing status. Below it Massachusetts is flat, and \"surtax\" comes back as 0.\n\n2026 rates, each cited to the state's own revenue department, constitution or code. The citation comes back in the \"source\" field of every answer, and the whole table is readable at /state-income-tax-rates. Michigan's rate, for instance, is re-determined each April and stayed at 4.25% for 2026.\n\nLocal income tax is answered where it has been verified and nowhere else. Pass \"locality\" for one of the rates modelled — Cincinnati 1.8%, Cleveland 2.5%, Columbus 2.5%, Philadelphia 3.735%, Philadelphia 3.425% for a non-resident who works there, Detroit 2.4%, New York City 3.078%–3.876%, Indianapolis (Marion County) 2.02%, Fort Wayne (Allen County) 1.59%, Gary and Hammond (Lake County) 1.5%, Carmel and Fishers (Hamilton County) 1.1%, South Bend (St. Joseph County) 1.75%, Evansville (Vanderburgh County) 1.25%, Lafayette (Tippecanoe County) 1.28%, Bloomington (Monroe County) 2.14%, Louisville 2.2%, Louisville 1.45% for a non-resident who works there, Lexington 2.75%, Lexington 2.25% for a non-resident who works there, Detroit 1.2% for a non-resident who works there, Grand Rapids 1.5%, Grand Rapids 0.75% for a non-resident who works there, Lansing 1%, Lansing 0.5% for a non-resident who works there, Flint 1%, Flint 0.5% for a non-resident who works there, Pontiac 1%, Pontiac 0.5% for a non-resident who works there, East Lansing 1%, East Lansing 0.5% for a non-resident who works there, Battle Creek 1%, Battle Creek 0.5% for a non-resident who works there, Saginaw 1.5%, Saginaw 0.75% for a non-resident who works there, Highland Park 2%, Highland Park 1% for a non-resident who works there, Muskegon 1%, Muskegon 0.5% for a non-resident who works there, Big Rapids 1%, Big Rapids 0.5% for a non-resident who works there, Walker 1%, Walker 0.5% for a non-resident who works there, Hamtramck 1%, Hamtramck 0.5% for a non-resident who works there, Muskegon Heights 1%, Muskegon Heights 0.5% for a non-resident who works there, Portland 1%, Portland 0.5% for a non-resident who works there, Benton Harbor 1%, Benton Harbor 0.5% for a non-resident who works there, Albion 1%, Albion 0.5% for a non-resident who works there, Ionia 1%, Ionia 0.5% for a non-resident who works there, Lapeer 1%, Lapeer 0.5% for a non-resident who works there, Springfield 1%, Springfield 0.5% for a non-resident who works there, Pittsburgh 3%, Pittsburgh 1% for a non-resident who works there, Allentown 1.975%, Allentown 1.28% for a non-resident who works there, Reading 3.6%, Reading 1% for a non-resident who works there, Erie 1.65%, Erie 1.65% for a non-resident who works there, Scranton 3.4%, Scranton 1% for a non-resident who works there, Bethlehem 1%, Bethlehem 1% for a non-resident who works there, Lancaster 1.6%, Lancaster 1% for a non-resident who works there, Harrisburg 2%, Harrisburg 1% for a non-resident who works there, Altoona 1.7%, Altoona 1.4% for a non-resident who works there, York 1.25%, York 1.25% for a non-resident who works there, Wilkes-Barre 3%, Wilkes-Barre 1% for a non-resident who works there, State College Borough 2.25%, State College Borough 1% for a non-resident who works there, Norristown Borough 2.1%, Norristown Borough 1% for a non-resident who works there, West Chester Borough 1.25%, West Chester Borough 1% for a non-resident who works there, Cheltenham Township 1.5%, Cheltenham Township 1% for a non-resident who works there, Upper Darby Township 1%, Bensalem Township 1%, Abington Township 1%, Millcreek Township 1%, Bristol Township 0.5%, Ross Township 1%, Mount Lebanon Township 1.3%, Toledo 2.5%, Akron 2.5%, Dayton 2.5%, Canton 2.5%, Springfield 2.4%, Kettering 2.25%, Parma 2.5%, Lorain 2.5%, Hamilton 2%, Euclid 2.85%, Cuyahoga Falls 2%, Middletown 2%, Mansfield 2.25%, Lakewood 1.5%, St. Louis 1% and Bowling Green 2% — and the answer comes back in \"localTax\", with \"stateAndLocalTax\" for the two lines together. \"localitiesAvailable\" lists what each state offers. They do not share a base, and the answer says which one it used rather than leaving you to assume: Ohio's cities and Philadelphia charge their rate on gross pay with no deduction and no exemption, so a 401(k) contribution does not reduce them (\"localTaxesPretaxRetirement\" is true), while Michigan's cities charge theirs on a federal wage figure the contribution was never in — the AGI on a Detroit resident's return, W-2 box 1 in every other city there — and each allows an exemption of its own ($600 a head in Detroit, Grand Rapids, Lansing, Flint, Pontiac, East Lansing, Highland Park, Muskegon, Big Rapids, Walker, Hamtramck, Muskegon Heights, Albion and Lapeer, and $700 a head in Ionia, and $750 a head in Battle Creek, Saginaw, Benton Harbor and Springfield, and $1000 a head in Portland), so a contribution does reduce all 40 of those rates and \"localExemptions\" is worth passing. New York City is the third base and the only locality here with a schedule rather than a rate: four resident brackets from 3.078% to 3.876% charged on New York State taxable income, so \"localRatePercent\" is null there and \"localBrackets\", \"localMarginalRatePercent\" and \"localTaxableIncome\" are how the answer is reproduced — and \"status\" changes it, because the state's standard deduction and the city's brackets both follow the filing status. Indiana's counties are the fourth base and the first to reuse one: eight of them charge a single rate on the same Indiana taxable income the state taxes, so \"localRatePercent\" and \"localTaxableIncome\" are both filled in and a 401(k) reduces the county line too. St. Louis is the fifth base and the first in a state this API cannot compute: its 1% earnings tax reaches a published list of compensation items — salaries, wages, bonuses, commissions, tips and severance — with deferred compensation and cafeteria plans on the city's own non-taxable list, so a 401(k) reduces it, there is no deduction or exemption to pass, and Missouri's own schedule is not modelled, which is why \"tax\" is null there and \"stateAndLocalTax\" is null with it. What is still missing is missing for the same reason the rest took this long, and Kansas City is the clearest case of it: its Code of Ordinances imposes the same 1% on residents' earnings and on work done in the city, but the RD-109 instructions that would settle whether a 401(k) deferral is inside that figure are served by a host that refuses every automated read, so the rate is absent rather than copied off a page the city does not publish. It will not be guessed at.\n\nOhio asks the local question twice, so this endpoint takes two local parameters and adds both. A municipal tax follows the **work** (\"locality\"); the school district income tax follows the **home** (\"schoolDistrict\"), and all 214 districts that levy one for 2026 are answered — so a Westerville resident working in Columbus owes Columbus's 2.5% on the work and Westerville City School District's 0.75% on the residence, and a request naming only one of them is short by the other. Name a district by its id or by the four-digit code on box 20 of the reader's own W-2, which is the only reliable way to match one: a district's boundaries are not a city's, so a bare name is deliberately not a spelling and \"schoolDistrictsAvailable\" carries the code beside every row. Ohio Revised Code 5748.01(E) lets a district choose one of two bases and they are not interchangeable, which is why \"schoolDistrictBase\" is in every answer: 68 charge their rate on wages with no deduction and no exemption, and 146 charge it on Ohio taxable income — Form SD 100 line 5, the state's personal exemption already off — so 1% of $75,000 is $750 in one and $729 in the other, and $708 filing jointly. Two things that follow surprise people: the state's $26,050 zero band and its $332 of flat dollars stay with the **state**, so $28,400 of wages owes Ohio nothing and owes a 1% district $260.50; and a 401(k) deferral reduces a district line and no Ohio city line, because both district bases are built on federal adjusted gross income and a municipal tax reaches box 5 Medicare wages.\n\nTwo rates come back, and they answer different questions. \"effectiveRatePercent\" is what this year cost; \"marginalRatePercent\" is what the next dollar costs, with the band it came from in \"marginalBracket\" and the whole schedule in \"brackets\". Do not clamp the marginal rate to \"ratePercent\": it is legitimately higher in Massachusetts above the surtax threshold, and in Utah, where a tapering credit takes 1.3 cents of every dollar between about $20,700 and $92,500 of single income, so the real rate on a raise there is 5.8% against an advertised 4.5%. Where a locality is asked for, \"stateAndLocalMarginalRatePercent\" adds the two.\n\nIncome tax is not the only thing a state takes out of a paycheck, and in 10 of them the second line is answered here too: \"payrollContributions\" is the disability, paid-leave and — in New Jersey alone — unemployment contribution withheld from the worker's own wages, with the programmes behind it in \"payrollContributionPrograms\" and the two lines added in \"stateTaxAndContributions\". It behaves like FICA and not like the tax above: charged on gross pay, so a 401(k) deferral does not reduce it, with a per-programme wage base rather than a deduction or a filing status. Washington's two contributions are the clearest case of why this cannot be folded into a rate — the state levies no income tax at all and still takes the most of any state here. Where nothing has been verified the field is null and not zero: Delaware's Paid Leave and Maryland's FAMLI belong in that table and are not in it yet, and New York's disability benefits contribution and Hawaii's TDI are left out because the law lets an employer pay them instead of setting the worker's share.\n\nThis is the state line only unless you ask for a locality. For the federal side — brackets, FICA and take-home pay — call /api/v1/paycheck and subtract this number.\n\nIdentical to the GET, with the parameters in a JSON object body.",
        "externalDocs": {
          "description": "The state income tax on wages calculator on farbetteroff.com",
          "url": "https://farbetteroff.com/calculators/take-home-paycheck-calculator"
        },
        "responses": {
          "200": {
            "$ref": "#/components/responses/state-tax-result"
          },
          "400": {
            "description": "A parameter is missing, mistyped, out of range or unknown — all of them are listed at once. A name that is one or two edits from a real parameter is answered with the one it probably meant; an ambiguous one is answered with the accepted list instead of a guess.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such endpoint. The response lists every valid slug.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "More than 60 requests in 60 seconds from one IP.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "A bug on our side. The inputs are never at fault for this one.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "operationId": "post-state-tax",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "state",
                  "wages"
                ],
                "properties": {
                  "state": {
                    "type": "string",
                    "enum": [
                      "AL",
                      "AK",
                      "AZ",
                      "AR",
                      "CA",
                      "CO",
                      "CT",
                      "DE",
                      "DC",
                      "FL",
                      "GA",
                      "HI",
                      "ID",
                      "IL",
                      "IN",
                      "IA",
                      "KS",
                      "KY",
                      "LA",
                      "ME",
                      "MD",
                      "MA",
                      "MI",
                      "MN",
                      "MS",
                      "MO",
                      "MT",
                      "NE",
                      "NV",
                      "NH",
                      "NJ",
                      "NM",
                      "NY",
                      "NC",
                      "ND",
                      "OH",
                      "OK",
                      "OR",
                      "PA",
                      "RI",
                      "SC",
                      "SD",
                      "TN",
                      "TX",
                      "UT",
                      "VT",
                      "VA",
                      "WA",
                      "WV",
                      "WI",
                      "WY",
                      "alabama",
                      "alaska",
                      "arizona",
                      "arkansas",
                      "california",
                      "colorado",
                      "connecticut",
                      "delaware",
                      "district of columbia",
                      "florida",
                      "georgia",
                      "hawaii",
                      "idaho",
                      "illinois",
                      "indiana",
                      "iowa",
                      "kansas",
                      "kentucky",
                      "louisiana",
                      "maine",
                      "maryland",
                      "massachusetts",
                      "michigan",
                      "minnesota",
                      "mississippi",
                      "missouri",
                      "montana",
                      "nebraska",
                      "nevada",
                      "new hampshire",
                      "new jersey",
                      "new mexico",
                      "new york",
                      "north carolina",
                      "north dakota",
                      "ohio",
                      "oklahoma",
                      "oregon",
                      "pennsylvania",
                      "rhode island",
                      "south carolina",
                      "south dakota",
                      "tennessee",
                      "texas",
                      "utah",
                      "vermont",
                      "virginia",
                      "washington",
                      "west virginia",
                      "wisconsin",
                      "wyoming",
                      "washington dc",
                      "washington d.c.",
                      "washington, dc",
                      "washington, d.c.",
                      "d.c.",
                      "washington state",
                      "new york state"
                    ],
                    "x-canonical-values": [
                      "AL",
                      "AK",
                      "AZ",
                      "AR",
                      "CA",
                      "CO",
                      "CT",
                      "DE",
                      "DC",
                      "FL",
                      "GA",
                      "HI",
                      "ID",
                      "IL",
                      "IN",
                      "IA",
                      "KS",
                      "KY",
                      "LA",
                      "ME",
                      "MD",
                      "MA",
                      "MI",
                      "MN",
                      "MS",
                      "MO",
                      "MT",
                      "NE",
                      "NV",
                      "NH",
                      "NJ",
                      "NM",
                      "NY",
                      "NC",
                      "ND",
                      "OH",
                      "OK",
                      "OR",
                      "PA",
                      "RI",
                      "SC",
                      "SD",
                      "TN",
                      "TX",
                      "UT",
                      "VT",
                      "VA",
                      "WA",
                      "WV",
                      "WI",
                      "WY"
                    ],
                    "examples": [
                      "OH"
                    ],
                    "description": "USPS two-letter code, or the full state name (\"Ohio\", \"District of Columbia\"). Where the bare name is taken, the spelling people actually write answers too — \"Washington DC\", \"Washington, D.C.\" and \"D.C.\" for the District, \"Washington State\" and \"New York State\" for the two states whose names those places open. Either case; the code is echoed back. Accepts one of AL, AK, AZ, AR, CA, CO, CT, DE, DC, FL, GA, HI, ID, IL, IN, IA, KS, KY, LA, ME, MD, MA, MI, MN, MS, MO, MT, NE, NV, NH, NJ, NM, NY, NC, ND, OH, OK, OR, PA, RI, SC, SD, TN, TX, UT, VT, VA, WA, WV, WI, WY. 58 of the listed spellings are aliases: they are resolved to one of 51 canonical values (`x-canonical-values`) before the answer echoes them. Matching ignores case."
                  },
                  "wages": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 1000000000,
                    "x-unit": "usd",
                    "examples": [
                      75000
                    ],
                    "description": "Gross annual wages. Accepts number, US dollars, between 0 and 1000000000."
                  },
                  "pretaxRetirement": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 1000000000,
                    "default": 0,
                    "x-unit": "usd",
                    "examples": [
                      3750
                    ],
                    "description": "Traditional 401(k)/403(b) dollars contributed over the year. Subtracted from the taxed wages everywhere except Pennsylvania, which taxes elective deferrals. Accepts number, US dollars, between 0 and 1000000000."
                  },
                  "filingStatus": {
                    "type": "string",
                    "enum": [
                      "single",
                      "married",
                      "hoh",
                      "mfs"
                    ],
                    "default": "single",
                    "description": "How this person files, which decides the state's standard deduction where we model one (Georgia exempts $15,000 single and $30,000 married filing jointly). Ignored in states whose deduction is not modelled. Accepts one of single, married, hoh, mfs."
                  },
                  "locality": {
                    "type": "string",
                    "enum": [
                      "oh-cincinnati",
                      "oh-cleveland",
                      "oh-columbus",
                      "pa-philadelphia",
                      "pa-philadelphia-nonresident",
                      "mi-detroit",
                      "ny-new-york-city",
                      "in-marion",
                      "in-allen",
                      "in-lake",
                      "in-hamilton",
                      "in-st-joseph",
                      "in-vanderburgh",
                      "in-tippecanoe",
                      "in-monroe",
                      "ky-louisville",
                      "ky-louisville-nonresident",
                      "ky-lexington",
                      "ky-lexington-nonresident",
                      "mi-detroit-nonresident",
                      "mi-grand-rapids",
                      "mi-grand-rapids-nonresident",
                      "mi-lansing",
                      "mi-lansing-nonresident",
                      "mi-flint",
                      "mi-flint-nonresident",
                      "mi-pontiac",
                      "mi-pontiac-nonresident",
                      "mi-east-lansing",
                      "mi-east-lansing-nonresident",
                      "mi-battle-creek",
                      "mi-battle-creek-nonresident",
                      "mi-saginaw",
                      "mi-saginaw-nonresident",
                      "mi-highland-park",
                      "mi-highland-park-nonresident",
                      "mi-muskegon",
                      "mi-muskegon-nonresident",
                      "mi-big-rapids",
                      "mi-big-rapids-nonresident",
                      "mi-walker",
                      "mi-walker-nonresident",
                      "mi-hamtramck",
                      "mi-hamtramck-nonresident",
                      "mi-muskegon-heights",
                      "mi-muskegon-heights-nonresident",
                      "mi-portland",
                      "mi-portland-nonresident",
                      "mi-benton-harbor",
                      "mi-benton-harbor-nonresident",
                      "mi-albion",
                      "mi-albion-nonresident",
                      "mi-ionia",
                      "mi-ionia-nonresident",
                      "mi-lapeer",
                      "mi-lapeer-nonresident",
                      "mi-springfield",
                      "mi-springfield-nonresident",
                      "pa-pittsburgh",
                      "pa-pittsburgh-nonresident",
                      "pa-allentown",
                      "pa-allentown-nonresident",
                      "pa-reading",
                      "pa-reading-nonresident",
                      "pa-erie",
                      "pa-erie-nonresident",
                      "pa-scranton",
                      "pa-scranton-nonresident",
                      "pa-bethlehem",
                      "pa-bethlehem-nonresident",
                      "pa-lancaster",
                      "pa-lancaster-nonresident",
                      "pa-harrisburg",
                      "pa-harrisburg-nonresident",
                      "pa-altoona",
                      "pa-altoona-nonresident",
                      "pa-york",
                      "pa-york-nonresident",
                      "pa-wilkes-barre",
                      "pa-wilkes-barre-nonresident",
                      "pa-state-college",
                      "pa-state-college-nonresident",
                      "pa-norristown",
                      "pa-norristown-nonresident",
                      "pa-west-chester",
                      "pa-west-chester-nonresident",
                      "pa-cheltenham",
                      "pa-cheltenham-nonresident",
                      "pa-upper-darby",
                      "pa-bensalem",
                      "pa-abington",
                      "pa-millcreek",
                      "pa-bristol-township",
                      "pa-ross-township",
                      "pa-mount-lebanon",
                      "oh-toledo",
                      "oh-akron",
                      "oh-dayton",
                      "oh-canton",
                      "oh-springfield",
                      "oh-kettering",
                      "oh-parma",
                      "oh-lorain",
                      "oh-hamilton",
                      "oh-euclid",
                      "oh-cuyahoga-falls",
                      "oh-middletown",
                      "oh-mansfield",
                      "oh-lakewood",
                      "mo-st-louis",
                      "ky-bowling-green",
                      "pittsburgh",
                      "allentown",
                      "reading",
                      "erie",
                      "scranton",
                      "bethlehem",
                      "lancaster",
                      "harrisburg",
                      "altoona",
                      "york",
                      "wilkes-barre",
                      "state-college",
                      "norristown",
                      "west-chester",
                      "cheltenham",
                      "upper-darby",
                      "bensalem",
                      "abington",
                      "millcreek",
                      "ross-township",
                      "mount-lebanon",
                      "mt-lebanon",
                      "bristol-township",
                      "cincinnati",
                      "cleveland",
                      "columbus",
                      "toledo",
                      "akron",
                      "dayton",
                      "canton",
                      "kettering",
                      "parma",
                      "lorain",
                      "euclid",
                      "cuyahoga-falls",
                      "middletown",
                      "mansfield",
                      "lakewood",
                      "hamilton-oh",
                      "hamilton-in",
                      "detroit",
                      "grand-rapids",
                      "lansing",
                      "flint",
                      "pontiac",
                      "east-lansing",
                      "battle-creek",
                      "saginaw",
                      "highland-park",
                      "muskegon",
                      "big-rapids",
                      "walker",
                      "hamtramck",
                      "muskegon-heights",
                      "portland",
                      "portland-mi",
                      "benton-harbor",
                      "albion",
                      "ionia",
                      "lapeer",
                      "springfield-mi",
                      "springfield-oh",
                      "philadelphia",
                      "nyc",
                      "new-york-city",
                      "indianapolis",
                      "fort-wayne",
                      "gary",
                      "hammond",
                      "carmel",
                      "fishers",
                      "south-bend",
                      "evansville",
                      "lafayette",
                      "bloomington",
                      "louisville",
                      "lexington",
                      "bowling-green",
                      "lexington-fayette",
                      "st-louis",
                      "st. louis",
                      "saint-louis",
                      "st-louis-city"
                    ],
                    "x-canonical-values": [
                      "oh-cincinnati",
                      "oh-cleveland",
                      "oh-columbus",
                      "pa-philadelphia",
                      "pa-philadelphia-nonresident",
                      "mi-detroit",
                      "ny-new-york-city",
                      "in-marion",
                      "in-allen",
                      "in-lake",
                      "in-hamilton",
                      "in-st-joseph",
                      "in-vanderburgh",
                      "in-tippecanoe",
                      "in-monroe",
                      "ky-louisville",
                      "ky-louisville-nonresident",
                      "ky-lexington",
                      "ky-lexington-nonresident",
                      "mi-detroit-nonresident",
                      "mi-grand-rapids",
                      "mi-grand-rapids-nonresident",
                      "mi-lansing",
                      "mi-lansing-nonresident",
                      "mi-flint",
                      "mi-flint-nonresident",
                      "mi-pontiac",
                      "mi-pontiac-nonresident",
                      "mi-east-lansing",
                      "mi-east-lansing-nonresident",
                      "mi-battle-creek",
                      "mi-battle-creek-nonresident",
                      "mi-saginaw",
                      "mi-saginaw-nonresident",
                      "mi-highland-park",
                      "mi-highland-park-nonresident",
                      "mi-muskegon",
                      "mi-muskegon-nonresident",
                      "mi-big-rapids",
                      "mi-big-rapids-nonresident",
                      "mi-walker",
                      "mi-walker-nonresident",
                      "mi-hamtramck",
                      "mi-hamtramck-nonresident",
                      "mi-muskegon-heights",
                      "mi-muskegon-heights-nonresident",
                      "mi-portland",
                      "mi-portland-nonresident",
                      "mi-benton-harbor",
                      "mi-benton-harbor-nonresident",
                      "mi-albion",
                      "mi-albion-nonresident",
                      "mi-ionia",
                      "mi-ionia-nonresident",
                      "mi-lapeer",
                      "mi-lapeer-nonresident",
                      "mi-springfield",
                      "mi-springfield-nonresident",
                      "pa-pittsburgh",
                      "pa-pittsburgh-nonresident",
                      "pa-allentown",
                      "pa-allentown-nonresident",
                      "pa-reading",
                      "pa-reading-nonresident",
                      "pa-erie",
                      "pa-erie-nonresident",
                      "pa-scranton",
                      "pa-scranton-nonresident",
                      "pa-bethlehem",
                      "pa-bethlehem-nonresident",
                      "pa-lancaster",
                      "pa-lancaster-nonresident",
                      "pa-harrisburg",
                      "pa-harrisburg-nonresident",
                      "pa-altoona",
                      "pa-altoona-nonresident",
                      "pa-york",
                      "pa-york-nonresident",
                      "pa-wilkes-barre",
                      "pa-wilkes-barre-nonresident",
                      "pa-state-college",
                      "pa-state-college-nonresident",
                      "pa-norristown",
                      "pa-norristown-nonresident",
                      "pa-west-chester",
                      "pa-west-chester-nonresident",
                      "pa-cheltenham",
                      "pa-cheltenham-nonresident",
                      "pa-upper-darby",
                      "pa-bensalem",
                      "pa-abington",
                      "pa-millcreek",
                      "pa-bristol-township",
                      "pa-ross-township",
                      "pa-mount-lebanon",
                      "oh-toledo",
                      "oh-akron",
                      "oh-dayton",
                      "oh-canton",
                      "oh-springfield",
                      "oh-kettering",
                      "oh-parma",
                      "oh-lorain",
                      "oh-hamilton",
                      "oh-euclid",
                      "oh-cuyahoga-falls",
                      "oh-middletown",
                      "oh-mansfield",
                      "oh-lakewood",
                      "mo-st-louis",
                      "ky-bowling-green"
                    ],
                    "examples": [
                      "oh-columbus"
                    ],
                    "description": "A local wage tax — a city, county, township or borough — to add on top of the state line, where Far Better Off models one. Omit it and every \"local\" field comes back null, which means not asked rather than none — read \"localitiesAvailable\" for what this state offers. The locality must sit in \"state\"; a mismatch is a 400 rather than a silent swap. Accepts one of oh-cincinnati, oh-cleveland, oh-columbus, pa-philadelphia, pa-philadelphia-nonresident, mi-detroit, ny-new-york-city, in-marion, in-allen, in-lake, in-hamilton, in-st-joseph, in-vanderburgh, in-tippecanoe, in-monroe, ky-louisville, ky-louisville-nonresident, ky-lexington, ky-lexington-nonresident, mi-detroit-nonresident, mi-grand-rapids, mi-grand-rapids-nonresident, mi-lansing, mi-lansing-nonresident, mi-flint, mi-flint-nonresident, mi-pontiac, mi-pontiac-nonresident, mi-east-lansing, mi-east-lansing-nonresident, mi-battle-creek, mi-battle-creek-nonresident, mi-saginaw, mi-saginaw-nonresident, mi-highland-park, mi-highland-park-nonresident, mi-muskegon, mi-muskegon-nonresident, mi-big-rapids, mi-big-rapids-nonresident, mi-walker, mi-walker-nonresident, mi-hamtramck, mi-hamtramck-nonresident, mi-muskegon-heights, mi-muskegon-heights-nonresident, mi-portland, mi-portland-nonresident, mi-benton-harbor, mi-benton-harbor-nonresident, mi-albion, mi-albion-nonresident, mi-ionia, mi-ionia-nonresident, mi-lapeer, mi-lapeer-nonresident, mi-springfield, mi-springfield-nonresident, pa-pittsburgh, pa-pittsburgh-nonresident, pa-allentown, pa-allentown-nonresident, pa-reading, pa-reading-nonresident, pa-erie, pa-erie-nonresident, pa-scranton, pa-scranton-nonresident, pa-bethlehem, pa-bethlehem-nonresident, pa-lancaster, pa-lancaster-nonresident, pa-harrisburg, pa-harrisburg-nonresident, pa-altoona, pa-altoona-nonresident, pa-york, pa-york-nonresident, pa-wilkes-barre, pa-wilkes-barre-nonresident, pa-state-college, pa-state-college-nonresident, pa-norristown, pa-norristown-nonresident, pa-west-chester, pa-west-chester-nonresident, pa-cheltenham, pa-cheltenham-nonresident, pa-upper-darby, pa-bensalem, pa-abington, pa-millcreek, pa-bristol-township, pa-ross-township, pa-mount-lebanon, oh-toledo, oh-akron, oh-dayton, oh-canton, oh-springfield, oh-kettering, oh-parma, oh-lorain, oh-hamilton, oh-euclid, oh-cuyahoga-falls, oh-middletown, oh-mansfield, oh-lakewood, mo-st-louis, ky-bowling-green. 83 of the listed spellings are aliases: they are resolved to one of 111 canonical values (`x-canonical-values`) before the answer echoes them. Matching ignores case."
                  },
                  "schoolDistrict": {
                    "type": "string",
                    "enum": [
                      "oh-sd-2301-amanda-clearcreek",
                      "oh-sd-0502-athens",
                      "oh-sd-2801-berkshire",
                      "oh-sd-2302-berne-union",
                      "oh-sd-5501-bethel",
                      "oh-sd-1401-blanchester",
                      "oh-sd-7502-botkins",
                      "oh-sd-5901-cardington-lincoln",
                      "oh-sd-5401-celina",
                      "oh-sd-8501-chippewa",
                      "oh-sd-6501-circleville",
                      "oh-sd-7001-clear-fork-valley",
                      "oh-sd-1402-clinton-massie",
                      "oh-sd-5204-cloverleaf",
                      "oh-sd-7201-clyde-green-springs",
                      "oh-sd-1704-crestline",
                      "oh-sd-8702-eastwood",
                      "oh-sd-5101-elgin",
                      "oh-sd-3204-findlay",
                      "oh-sd-0404-geneva-area",
                      "oh-sd-7203-gibsonburg",
                      "oh-sd-8503-green",
                      "oh-sd-3603-greenfield",
                      "oh-sd-0302-hillsdale",
                      "oh-sd-7403-hopewell-loudon",
                      "oh-sd-7506-jackson-center",
                      "oh-sd-6704-james-a-garfield",
                      "oh-sd-4901-jefferson",
                      "oh-sd-4902-jonathan-alder",
                      "oh-sd-8303-kings",
                      "oh-sd-2305-lancaster",
                      "oh-sd-6502-logan-elm",
                      "oh-sd-4904-madison-plains",
                      "oh-sd-5403-marion",
                      "oh-sd-5504-miami-east",
                      "oh-sd-5505-milton-union",
                      "oh-sd-3902-monroeville",
                      "oh-sd-8605-montpelier",
                      "oh-sd-8705-north-baltimore",
                      "oh-sd-4508-north-fork",
                      "oh-sd-1203-northeastern",
                      "oh-sd-4509-northridge",
                      "oh-sd-7612-northwest",
                      "oh-sd-1204-northwestern",
                      "oh-sd-8706-northwood",
                      "oh-sd-7711-norton",
                      "oh-sd-8504-norwayne",
                      "oh-sd-5103-pleasant",
                      "oh-sd-5104-ridgedale",
                      "oh-sd-5105-river-valley",
                      "oh-sd-4604-riverside",
                      "oh-sd-0908-ross",
                      "oh-sd-5008-sebring",
                      "oh-sd-7508-sidney",
                      "oh-sd-3118-southwest",
                      "oh-sd-0604-st-marys",
                      "oh-sd-6503-teays-valley",
                      "oh-sd-7407-tiffin",
                      "oh-sd-6806-tri-county-north",
                      "oh-sd-0505-trimble",
                      "oh-sd-8509-triway",
                      "oh-sd-5509-troy",
                      "oh-sd-2308-walnut-township",
                      "oh-sd-2402-washington-court-house",
                      "oh-sd-2607-wauseon",
                      "oh-sd-2514-westerville",
                      "oh-sd-3907-willard",
                      "oh-sd-7107-zane-trace",
                      "oh-sd-3301-ada",
                      "oh-sd-7501-anna",
                      "oh-sd-1901-ansonia",
                      "oh-sd-6301-antwerp",
                      "oh-sd-3201-arcadia",
                      "oh-sd-1902-arcanum-butler",
                      "oh-sd-3202-arlington",
                      "oh-sd-2001-ayersville",
                      "oh-sd-3901-bellevue",
                      "oh-sd-2501-bexley",
                      "oh-sd-2101-big-walnut",
                      "oh-sd-2303-bloom-carroll",
                      "oh-sd-0203-bluffton",
                      "oh-sd-8701-bowling-green",
                      "oh-sd-5502-bradford",
                      "oh-sd-8601-bryan",
                      "oh-sd-1701-buckeye-central",
                      "oh-sd-2102-buckeye-valley",
                      "oh-sd-2502-canal-winchester",
                      "oh-sd-8801-carey",
                      "oh-sd-8301-carlisle",
                      "oh-sd-2902-cedar-cliff",
                      "oh-sd-4201-centerburg",
                      "oh-sd-2002-central",
                      "oh-sd-1303-clermont-northeastern",
                      "oh-sd-5402-coldwater",
                      "oh-sd-1703-colonel-crawford",
                      "oh-sd-1502-columbiana",
                      "oh-sd-6901-columbus-grove",
                      "oh-sd-6902-continental",
                      "oh-sd-3203-cory-rawson",
                      "oh-sd-5503-covington",
                      "oh-sd-1503-crestview",
                      "oh-sd-8101-crestview",
                      "oh-sd-8502-dalton",
                      "oh-sd-4202-danville",
                      "oh-sd-2003-defiance",
                      "oh-sd-0204-delphos",
                      "oh-sd-6803-eaton",
                      "oh-sd-8602-edgerton",
                      "oh-sd-8703-elmwood",
                      "oh-sd-2602-evergreen",
                      "oh-sd-8001-fairbanks",
                      "oh-sd-2903-fairborn",
                      "oh-sd-2304-fairfield-union",
                      "oh-sd-7503-fairlawn",
                      "oh-sd-2603-fayette",
                      "oh-sd-7504-fort-loramie",
                      "oh-sd-5406-fort-recovery",
                      "oh-sd-1903-franklin-monroe",
                      "oh-sd-7202-fremont",
                      "oh-sd-1305-goshen",
                      "oh-sd-4501-granville",
                      "oh-sd-2904-greeneview",
                      "oh-sd-1904-greenville",
                      "oh-sd-3302-hardin-northern",
                      "oh-sd-7505-hardin-houston",
                      "oh-sd-2004-hicksville",
                      "oh-sd-5902-highland",
                      "oh-sd-3604-hillsboro",
                      "oh-sd-3501-holgate",
                      "oh-sd-6903-jennings",
                      "oh-sd-4503-johnstown-monroe",
                      "oh-sd-6904-kalida",
                      "oh-sd-3303-kenton",
                      "oh-sd-7204-lakota",
                      "oh-sd-6905-leipsic",
                      "oh-sd-3502-liberty-center",
                      "oh-sd-2306-liberty-union-thurston",
                      "oh-sd-3205-liberty-benton",
                      "oh-sd-4506-licking-valley",
                      "oh-sd-4903-london",
                      "oh-sd-0303-loudonville-perrysville",
                      "oh-sd-0905-madison",
                      "oh-sd-3206-mccomb",
                      "oh-sd-1102-mechanicsburg",
                      "oh-sd-8604-millcreek-west-unity",
                      "oh-sd-6906-miller-city-new-cleveland",
                      "oh-sd-0601-minster",
                      "oh-sd-1905-mississinawa-valley",
                      "oh-sd-8802-mohawk",
                      "oh-sd-5903-mount-gilead",
                      "oh-sd-6802-national-trail",
                      "oh-sd-0602-new-bremen",
                      "oh-sd-0603-new-knoxville",
                      "oh-sd-5708-new-lebanon",
                      "oh-sd-3903-new-london",
                      "oh-sd-0907-new-miami",
                      "oh-sd-7404-new-riegel",
                      "oh-sd-4507-newark",
                      "oh-sd-5506-newton",
                      "oh-sd-8003-north-union",
                      "oh-sd-5904-northmor",
                      "oh-sd-8505-northwestern",
                      "oh-sd-3904-norwalk",
                      "oh-sd-4712-oberlin",
                      "oh-sd-7405-old-fort",
                      "oh-sd-8707-otsego",
                      "oh-sd-6907-ottawa-glandorf",
                      "oh-sd-6908-ottoville",
                      "oh-sd-6909-pandora-gilboa",
                      "oh-sd-5405-parkway",
                      "oh-sd-3504-patrick-henry",
                      "oh-sd-6302-paulding",
                      "oh-sd-8708-perrysburg",
                      "oh-sd-2604-pettisville",
                      "oh-sd-2307-pickerington",
                      "oh-sd-2605-pike-delta-york",
                      "oh-sd-5507-piqua",
                      "oh-sd-7007-plymouth-shiloh",
                      "oh-sd-6804-preble-shawnee",
                      "oh-sd-2509-reynoldsburg",
                      "oh-sd-3304-ridgemont",
                      "oh-sd-3305-riverdale",
                      "oh-sd-7507-russia",
                      "oh-sd-7406-seneca-east",
                      "oh-sd-7008-shelby",
                      "oh-sd-3905-south-central",
                      "oh-sd-1205-southeastern",
                      "oh-sd-4510-southwest-licking",
                      "oh-sd-0209-spencerville",
                      "oh-sd-5010-springfield",
                      "oh-sd-8607-stryker",
                      "oh-sd-2606-swanton",
                      "oh-sd-0909-talawanda",
                      "oh-sd-1906-tri-village",
                      "oh-sd-1103-triad",
                      "oh-sd-6805-twin-valley-community",
                      "oh-sd-7106-union-scioto",
                      "oh-sd-1510-united",
                      "oh-sd-8803-upper-sandusky",
                      "oh-sd-3306-upper-scioto-valley",
                      "oh-sd-5713-valley-view",
                      "oh-sd-3207-van-buren",
                      "oh-sd-8104-van-wert",
                      "oh-sd-3208-vanlue",
                      "oh-sd-1907-versailles",
                      "oh-sd-0605-wapakoneta",
                      "oh-sd-6303-wayne-trace",
                      "oh-sd-0606-waynesfield-goshen",
                      "oh-sd-4715-wellington",
                      "oh-sd-1105-west-liberty-salem",
                      "oh-sd-3906-western-reserve",
                      "oh-sd-3122-wyoming",
                      "oh-sd-2906-xenia-community",
                      "oh-sd-2907-yellow-springs",
                      "1102",
                      "1103",
                      "1105",
                      "1203",
                      "1204",
                      "1205",
                      "1303",
                      "1305",
                      "1401",
                      "1402",
                      "1502",
                      "1503",
                      "1510",
                      "1701",
                      "1703",
                      "1704",
                      "1901",
                      "1902",
                      "1903",
                      "1904",
                      "1905",
                      "1906",
                      "1907",
                      "2001",
                      "2002",
                      "2003",
                      "2004",
                      "2101",
                      "2102",
                      "2301",
                      "2302",
                      "2303",
                      "2304",
                      "2305",
                      "2306",
                      "2307",
                      "2308",
                      "2402",
                      "2501",
                      "2502",
                      "2509",
                      "2514",
                      "2602",
                      "2603",
                      "2604",
                      "2605",
                      "2606",
                      "2607",
                      "2801",
                      "2902",
                      "2903",
                      "2904",
                      "2906",
                      "2907",
                      "3118",
                      "3122",
                      "3201",
                      "3202",
                      "3203",
                      "3204",
                      "3205",
                      "3206",
                      "3207",
                      "3208",
                      "3301",
                      "3302",
                      "3303",
                      "3304",
                      "3305",
                      "3306",
                      "3501",
                      "3502",
                      "3504",
                      "3603",
                      "3604",
                      "3901",
                      "3902",
                      "3903",
                      "3904",
                      "3905",
                      "3906",
                      "3907",
                      "4201",
                      "4202",
                      "4501",
                      "4503",
                      "4506",
                      "4507",
                      "4508",
                      "4509",
                      "4510",
                      "4604",
                      "4712",
                      "4715",
                      "4901",
                      "4902",
                      "4903",
                      "4904",
                      "5008",
                      "5010",
                      "5101",
                      "5103",
                      "5104",
                      "5105",
                      "5204",
                      "5401",
                      "5402",
                      "5403",
                      "5405",
                      "5406",
                      "5501",
                      "5502",
                      "5503",
                      "5504",
                      "5505",
                      "5506",
                      "5507",
                      "5509",
                      "5708",
                      "5713",
                      "5901",
                      "5902",
                      "5903",
                      "5904",
                      "6301",
                      "6302",
                      "6303",
                      "6501",
                      "6502",
                      "6503",
                      "6704",
                      "6802",
                      "6803",
                      "6804",
                      "6805",
                      "6806",
                      "6901",
                      "6902",
                      "6903",
                      "6904",
                      "6905",
                      "6906",
                      "6907",
                      "6908",
                      "6909",
                      "7001",
                      "7007",
                      "7008",
                      "7106",
                      "7107",
                      "7201",
                      "7202",
                      "7203",
                      "7204",
                      "7403",
                      "7404",
                      "7405",
                      "7406",
                      "7407",
                      "7501",
                      "7502",
                      "7503",
                      "7504",
                      "7505",
                      "7506",
                      "7507",
                      "7508",
                      "7612",
                      "7711",
                      "8001",
                      "8003",
                      "8101",
                      "8104",
                      "8301",
                      "8303",
                      "8501",
                      "8502",
                      "8503",
                      "8504",
                      "8505",
                      "8509",
                      "8601",
                      "8602",
                      "8604",
                      "8605",
                      "8607",
                      "8701",
                      "8702",
                      "8703",
                      "8705",
                      "8706",
                      "8707",
                      "8708",
                      "8801",
                      "8802",
                      "8803",
                      "0502",
                      "0404",
                      "0302",
                      "0908",
                      "0604",
                      "0505",
                      "0203",
                      "0204",
                      "0303",
                      "0905",
                      "0601",
                      "0602",
                      "0603",
                      "0907",
                      "0209",
                      "0909",
                      "0605",
                      "0606"
                    ],
                    "x-canonical-values": [
                      "oh-sd-2301-amanda-clearcreek",
                      "oh-sd-0502-athens",
                      "oh-sd-2801-berkshire",
                      "oh-sd-2302-berne-union",
                      "oh-sd-5501-bethel",
                      "oh-sd-1401-blanchester",
                      "oh-sd-7502-botkins",
                      "oh-sd-5901-cardington-lincoln",
                      "oh-sd-5401-celina",
                      "oh-sd-8501-chippewa",
                      "oh-sd-6501-circleville",
                      "oh-sd-7001-clear-fork-valley",
                      "oh-sd-1402-clinton-massie",
                      "oh-sd-5204-cloverleaf",
                      "oh-sd-7201-clyde-green-springs",
                      "oh-sd-1704-crestline",
                      "oh-sd-8702-eastwood",
                      "oh-sd-5101-elgin",
                      "oh-sd-3204-findlay",
                      "oh-sd-0404-geneva-area",
                      "oh-sd-7203-gibsonburg",
                      "oh-sd-8503-green",
                      "oh-sd-3603-greenfield",
                      "oh-sd-0302-hillsdale",
                      "oh-sd-7403-hopewell-loudon",
                      "oh-sd-7506-jackson-center",
                      "oh-sd-6704-james-a-garfield",
                      "oh-sd-4901-jefferson",
                      "oh-sd-4902-jonathan-alder",
                      "oh-sd-8303-kings",
                      "oh-sd-2305-lancaster",
                      "oh-sd-6502-logan-elm",
                      "oh-sd-4904-madison-plains",
                      "oh-sd-5403-marion",
                      "oh-sd-5504-miami-east",
                      "oh-sd-5505-milton-union",
                      "oh-sd-3902-monroeville",
                      "oh-sd-8605-montpelier",
                      "oh-sd-8705-north-baltimore",
                      "oh-sd-4508-north-fork",
                      "oh-sd-1203-northeastern",
                      "oh-sd-4509-northridge",
                      "oh-sd-7612-northwest",
                      "oh-sd-1204-northwestern",
                      "oh-sd-8706-northwood",
                      "oh-sd-7711-norton",
                      "oh-sd-8504-norwayne",
                      "oh-sd-5103-pleasant",
                      "oh-sd-5104-ridgedale",
                      "oh-sd-5105-river-valley",
                      "oh-sd-4604-riverside",
                      "oh-sd-0908-ross",
                      "oh-sd-5008-sebring",
                      "oh-sd-7508-sidney",
                      "oh-sd-3118-southwest",
                      "oh-sd-0604-st-marys",
                      "oh-sd-6503-teays-valley",
                      "oh-sd-7407-tiffin",
                      "oh-sd-6806-tri-county-north",
                      "oh-sd-0505-trimble",
                      "oh-sd-8509-triway",
                      "oh-sd-5509-troy",
                      "oh-sd-2308-walnut-township",
                      "oh-sd-2402-washington-court-house",
                      "oh-sd-2607-wauseon",
                      "oh-sd-2514-westerville",
                      "oh-sd-3907-willard",
                      "oh-sd-7107-zane-trace",
                      "oh-sd-3301-ada",
                      "oh-sd-7501-anna",
                      "oh-sd-1901-ansonia",
                      "oh-sd-6301-antwerp",
                      "oh-sd-3201-arcadia",
                      "oh-sd-1902-arcanum-butler",
                      "oh-sd-3202-arlington",
                      "oh-sd-2001-ayersville",
                      "oh-sd-3901-bellevue",
                      "oh-sd-2501-bexley",
                      "oh-sd-2101-big-walnut",
                      "oh-sd-2303-bloom-carroll",
                      "oh-sd-0203-bluffton",
                      "oh-sd-8701-bowling-green",
                      "oh-sd-5502-bradford",
                      "oh-sd-8601-bryan",
                      "oh-sd-1701-buckeye-central",
                      "oh-sd-2102-buckeye-valley",
                      "oh-sd-2502-canal-winchester",
                      "oh-sd-8801-carey",
                      "oh-sd-8301-carlisle",
                      "oh-sd-2902-cedar-cliff",
                      "oh-sd-4201-centerburg",
                      "oh-sd-2002-central",
                      "oh-sd-1303-clermont-northeastern",
                      "oh-sd-5402-coldwater",
                      "oh-sd-1703-colonel-crawford",
                      "oh-sd-1502-columbiana",
                      "oh-sd-6901-columbus-grove",
                      "oh-sd-6902-continental",
                      "oh-sd-3203-cory-rawson",
                      "oh-sd-5503-covington",
                      "oh-sd-1503-crestview",
                      "oh-sd-8101-crestview",
                      "oh-sd-8502-dalton",
                      "oh-sd-4202-danville",
                      "oh-sd-2003-defiance",
                      "oh-sd-0204-delphos",
                      "oh-sd-6803-eaton",
                      "oh-sd-8602-edgerton",
                      "oh-sd-8703-elmwood",
                      "oh-sd-2602-evergreen",
                      "oh-sd-8001-fairbanks",
                      "oh-sd-2903-fairborn",
                      "oh-sd-2304-fairfield-union",
                      "oh-sd-7503-fairlawn",
                      "oh-sd-2603-fayette",
                      "oh-sd-7504-fort-loramie",
                      "oh-sd-5406-fort-recovery",
                      "oh-sd-1903-franklin-monroe",
                      "oh-sd-7202-fremont",
                      "oh-sd-1305-goshen",
                      "oh-sd-4501-granville",
                      "oh-sd-2904-greeneview",
                      "oh-sd-1904-greenville",
                      "oh-sd-3302-hardin-northern",
                      "oh-sd-7505-hardin-houston",
                      "oh-sd-2004-hicksville",
                      "oh-sd-5902-highland",
                      "oh-sd-3604-hillsboro",
                      "oh-sd-3501-holgate",
                      "oh-sd-6903-jennings",
                      "oh-sd-4503-johnstown-monroe",
                      "oh-sd-6904-kalida",
                      "oh-sd-3303-kenton",
                      "oh-sd-7204-lakota",
                      "oh-sd-6905-leipsic",
                      "oh-sd-3502-liberty-center",
                      "oh-sd-2306-liberty-union-thurston",
                      "oh-sd-3205-liberty-benton",
                      "oh-sd-4506-licking-valley",
                      "oh-sd-4903-london",
                      "oh-sd-0303-loudonville-perrysville",
                      "oh-sd-0905-madison",
                      "oh-sd-3206-mccomb",
                      "oh-sd-1102-mechanicsburg",
                      "oh-sd-8604-millcreek-west-unity",
                      "oh-sd-6906-miller-city-new-cleveland",
                      "oh-sd-0601-minster",
                      "oh-sd-1905-mississinawa-valley",
                      "oh-sd-8802-mohawk",
                      "oh-sd-5903-mount-gilead",
                      "oh-sd-6802-national-trail",
                      "oh-sd-0602-new-bremen",
                      "oh-sd-0603-new-knoxville",
                      "oh-sd-5708-new-lebanon",
                      "oh-sd-3903-new-london",
                      "oh-sd-0907-new-miami",
                      "oh-sd-7404-new-riegel",
                      "oh-sd-4507-newark",
                      "oh-sd-5506-newton",
                      "oh-sd-8003-north-union",
                      "oh-sd-5904-northmor",
                      "oh-sd-8505-northwestern",
                      "oh-sd-3904-norwalk",
                      "oh-sd-4712-oberlin",
                      "oh-sd-7405-old-fort",
                      "oh-sd-8707-otsego",
                      "oh-sd-6907-ottawa-glandorf",
                      "oh-sd-6908-ottoville",
                      "oh-sd-6909-pandora-gilboa",
                      "oh-sd-5405-parkway",
                      "oh-sd-3504-patrick-henry",
                      "oh-sd-6302-paulding",
                      "oh-sd-8708-perrysburg",
                      "oh-sd-2604-pettisville",
                      "oh-sd-2307-pickerington",
                      "oh-sd-2605-pike-delta-york",
                      "oh-sd-5507-piqua",
                      "oh-sd-7007-plymouth-shiloh",
                      "oh-sd-6804-preble-shawnee",
                      "oh-sd-2509-reynoldsburg",
                      "oh-sd-3304-ridgemont",
                      "oh-sd-3305-riverdale",
                      "oh-sd-7507-russia",
                      "oh-sd-7406-seneca-east",
                      "oh-sd-7008-shelby",
                      "oh-sd-3905-south-central",
                      "oh-sd-1205-southeastern",
                      "oh-sd-4510-southwest-licking",
                      "oh-sd-0209-spencerville",
                      "oh-sd-5010-springfield",
                      "oh-sd-8607-stryker",
                      "oh-sd-2606-swanton",
                      "oh-sd-0909-talawanda",
                      "oh-sd-1906-tri-village",
                      "oh-sd-1103-triad",
                      "oh-sd-6805-twin-valley-community",
                      "oh-sd-7106-union-scioto",
                      "oh-sd-1510-united",
                      "oh-sd-8803-upper-sandusky",
                      "oh-sd-3306-upper-scioto-valley",
                      "oh-sd-5713-valley-view",
                      "oh-sd-3207-van-buren",
                      "oh-sd-8104-van-wert",
                      "oh-sd-3208-vanlue",
                      "oh-sd-1907-versailles",
                      "oh-sd-0605-wapakoneta",
                      "oh-sd-6303-wayne-trace",
                      "oh-sd-0606-waynesfield-goshen",
                      "oh-sd-4715-wellington",
                      "oh-sd-1105-west-liberty-salem",
                      "oh-sd-3906-western-reserve",
                      "oh-sd-3122-wyoming",
                      "oh-sd-2906-xenia-community",
                      "oh-sd-2907-yellow-springs"
                    ],
                    "examples": [
                      "2514"
                    ],
                    "description": "Ohio's school district income tax, which is a second local levy on the same paycheck and the only one here charged by where you **live** rather than where you work — so it is owed on top of \"locality\" and not instead of it. All 214 districts that levy one for 2026 are accepted, by id or by the four-digit code from box 20 of your W-2 (\"2301\"), which is the only reliable way to match one: a district's boundaries are not a city's, a township's or a ZIP code's, so a bare name is deliberately not a spelling. Omit it and every \"schoolDistrict\" field comes back null, which means not asked rather than none — read \"schoolDistrictsAvailable\" for what this state offers. The district must sit in \"state\"; a mismatch is a 400 rather than a silent swap. Accepts one of 214 Ohio school district ids (oh-sd-2301-amanda-clearcreek), or the four-digit district code from box 20 of your W-2 (2301) — the list is in \"schoolDistrictsAvailable\" on any answer for state=OH. 214 of the listed spellings are aliases: they are resolved to one of 214 canonical values (`x-canonical-values`) before the answer echoes them. Matching ignores case."
                  },
                  "localExemptions": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 20,
                    "default": 0,
                    "description": "Personal and dependency exemptions to claim against the locality's tax, as a head count — the filer, a spouse, each dependent. Only Michigan's 40 rates allow any of the localities modelled ($600 a head in Detroit, Grand Rapids, Lansing, Flint, Pontiac, East Lansing, Highland Park, Muskegon, Big Rapids, Walker, Hamtramck, Muskegon Heights, Albion and Lapeer, and $700 a head in Ionia, and $750 a head in Battle Creek, Saginaw, Benton Harbor and Springfield, and $1000 a head in Portland, read back in \"localExemptionAmount\"); elsewhere it changes nothing. The default of 0 is the larger bill, so an omitted count never understates a city. Accepts number, between 0 and 20."
                  }
                }
              }
            }
          }
        }
      }
    },
    "/loan": {
      "get": {
        "tags": [
          "calculators"
        ],
        "summary": "Loan payment & amortization",
        "description": "Monthly payment, total interest and the year-by-year schedule for any fixed-rate amortizing loan — mortgage, auto, student or personal.",
        "externalDocs": {
          "description": "The loan payment & amortization calculator on farbetteroff.com",
          "url": "https://farbetteroff.com/calculators/personal-loan-calculator"
        },
        "responses": {
          "200": {
            "$ref": "#/components/responses/loan-result"
          },
          "400": {
            "description": "A parameter is missing, mistyped, out of range or unknown — all of them are listed at once. A name that is one or two edits from a real parameter is answered with the one it probably meant; an ambiguous one is answered with the accepted list instead of a guess.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such endpoint. The response lists every valid slug.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "More than 60 requests in 60 seconds from one IP.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "A bug on our side. The inputs are never at fault for this one.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "operationId": "get-loan",
        "parameters": [
          {
            "name": "principal",
            "in": "query",
            "required": true,
            "description": "Amount borrowed. Accepts number, US dollars, between 0 and 100000000000.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 100000000000,
              "x-unit": "usd",
              "examples": [
                300000
              ]
            }
          },
          {
            "name": "rate",
            "in": "query",
            "required": true,
            "description": "Annual interest rate (APR). Accepts number, a percentage, so 6.5 means 6.5%, between 0 and 100.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 100,
              "x-unit": "percent",
              "examples": [
                6.5
              ]
            }
          },
          {
            "name": "termMonths",
            "in": "query",
            "required": true,
            "description": "Length of the loan in months. A 30-year mortgage is 360. Accepts integer, whole months, between 1 and 1200.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 1200,
              "x-unit": "months",
              "examples": [
                360
              ]
            }
          },
          {
            "name": "extraMonthly",
            "in": "query",
            "required": false,
            "description": "Extra principal paid on top of the scheduled payment each month. Accepts number, US dollars, between 0 and 1000000000.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 1000000000,
              "default": 0,
              "x-unit": "usd",
              "examples": [
                200
              ]
            }
          },
          {
            "name": "schedule",
            "in": "query",
            "required": false,
            "description": "Include the year-by-year amortization schedule in the response. Accepts boolean.",
            "schema": {
              "type": "boolean",
              "default": false
            }
          }
        ]
      },
      "post": {
        "tags": [
          "calculators"
        ],
        "summary": "Loan payment & amortization",
        "description": "Monthly payment, total interest and the year-by-year schedule for any fixed-rate amortizing loan — mortgage, auto, student or personal.\n\nIdentical to the GET, with the parameters in a JSON object body.",
        "externalDocs": {
          "description": "The loan payment & amortization calculator on farbetteroff.com",
          "url": "https://farbetteroff.com/calculators/personal-loan-calculator"
        },
        "responses": {
          "200": {
            "$ref": "#/components/responses/loan-result"
          },
          "400": {
            "description": "A parameter is missing, mistyped, out of range or unknown — all of them are listed at once. A name that is one or two edits from a real parameter is answered with the one it probably meant; an ambiguous one is answered with the accepted list instead of a guess.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such endpoint. The response lists every valid slug.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "More than 60 requests in 60 seconds from one IP.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "A bug on our side. The inputs are never at fault for this one.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "operationId": "post-loan",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "principal",
                  "rate",
                  "termMonths"
                ],
                "properties": {
                  "principal": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 100000000000,
                    "x-unit": "usd",
                    "examples": [
                      300000
                    ],
                    "description": "Amount borrowed. Accepts number, US dollars, between 0 and 100000000000."
                  },
                  "rate": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 100,
                    "x-unit": "percent",
                    "examples": [
                      6.5
                    ],
                    "description": "Annual interest rate (APR). Accepts number, a percentage, so 6.5 means 6.5%, between 0 and 100."
                  },
                  "termMonths": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 1200,
                    "x-unit": "months",
                    "examples": [
                      360
                    ],
                    "description": "Length of the loan in months. A 30-year mortgage is 360. Accepts integer, whole months, between 1 and 1200."
                  },
                  "extraMonthly": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 1000000000,
                    "default": 0,
                    "x-unit": "usd",
                    "examples": [
                      200
                    ],
                    "description": "Extra principal paid on top of the scheduled payment each month. Accepts number, US dollars, between 0 and 1000000000."
                  },
                  "schedule": {
                    "type": "boolean",
                    "default": false,
                    "description": "Include the year-by-year amortization schedule in the response. Accepts boolean."
                  }
                }
              }
            }
          }
        }
      }
    },
    "/mortgage": {
      "get": {
        "tags": [
          "calculators"
        ],
        "summary": "Mortgage payment (PITI)",
        "description": "The whole monthly payment on a home loan — principal, interest, property tax, insurance, HOA and PMI — not just principal and interest.\n\nPMI termination follows the Homeowners Protection Act as summarised by the CFPB: automatic at 78% of the original value, or the amortization midpoint as a backstop; borrower-requested at 80%.\n\nProperty tax and insurance are whatever you pass in — Far Better Off does not look up local rates.",
        "externalDocs": {
          "description": "The mortgage payment (piti) calculator on farbetteroff.com",
          "url": "https://farbetteroff.com/calculators/mortgage-calculator"
        },
        "responses": {
          "200": {
            "$ref": "#/components/responses/mortgage-result"
          },
          "400": {
            "description": "A parameter is missing, mistyped, out of range or unknown — all of them are listed at once. A name that is one or two edits from a real parameter is answered with the one it probably meant; an ambiguous one is answered with the accepted list instead of a guess.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such endpoint. The response lists every valid slug.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "More than 60 requests in 60 seconds from one IP.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "A bug on our side. The inputs are never at fault for this one.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "operationId": "get-mortgage",
        "parameters": [
          {
            "name": "homePrice",
            "in": "query",
            "required": true,
            "description": "Purchase price. Accepts number, US dollars, between 0 and 10000000000.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 10000000000,
              "x-unit": "usd",
              "examples": [
                400000
              ]
            }
          },
          {
            "name": "downPayment",
            "in": "query",
            "required": false,
            "description": "Down payment in dollars. Accepts number, US dollars, between 0 and 10000000000.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 10000000000,
              "default": 0,
              "x-unit": "usd",
              "examples": [
                80000
              ]
            }
          },
          {
            "name": "rate",
            "in": "query",
            "required": true,
            "description": "Annual interest rate (APR). Accepts number, a percentage, so 6.5 means 6.5%, between 0 and 100.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 100,
              "x-unit": "percent",
              "examples": [
                6.5
              ]
            }
          },
          {
            "name": "termYears",
            "in": "query",
            "required": false,
            "description": "Loan term in years. Accepts integer, years, between 1 and 50.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 50,
              "default": 30,
              "x-unit": "years",
              "examples": [
                30
              ]
            }
          },
          {
            "name": "propertyTaxAnnual",
            "in": "query",
            "required": false,
            "description": "Property tax per year. Accepts number, US dollars, between 0 and 10000000.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 10000000,
              "default": 0,
              "x-unit": "usd",
              "examples": [
                3600
              ]
            }
          },
          {
            "name": "insuranceAnnual",
            "in": "query",
            "required": false,
            "description": "Homeowner's insurance per year. Accepts number, US dollars, between 0 and 10000000.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 10000000,
              "default": 0,
              "x-unit": "usd",
              "examples": [
                1800
              ]
            }
          },
          {
            "name": "hoaMonthly",
            "in": "query",
            "required": false,
            "description": "HOA dues per month. Accepts number, US dollars, between 0 and 1000000.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 1000000,
              "default": 0,
              "x-unit": "usd"
            }
          },
          {
            "name": "pmiRate",
            "in": "query",
            "required": false,
            "description": "Annual PMI premium as a percentage of the loan. Only charged below 20% down. Freddie Mac puts a typical premium at $30–$70 a month per $100,000 borrowed, which is 0.36%–0.84% of the loan a year. Accepts number, a percentage, so 6.5 means 6.5%, between 0 and 5.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 5,
              "default": 0.5,
              "x-unit": "percent"
            }
          },
          {
            "name": "extraMonthly",
            "in": "query",
            "required": false,
            "description": "Extra principal per month. Accepts number, US dollars, between 0 and 10000000.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 10000000,
              "default": 0,
              "x-unit": "usd"
            }
          }
        ]
      },
      "post": {
        "tags": [
          "calculators"
        ],
        "summary": "Mortgage payment (PITI)",
        "description": "The whole monthly payment on a home loan — principal, interest, property tax, insurance, HOA and PMI — not just principal and interest.\n\nPMI termination follows the Homeowners Protection Act as summarised by the CFPB: automatic at 78% of the original value, or the amortization midpoint as a backstop; borrower-requested at 80%.\n\nProperty tax and insurance are whatever you pass in — Far Better Off does not look up local rates.\n\nIdentical to the GET, with the parameters in a JSON object body.",
        "externalDocs": {
          "description": "The mortgage payment (piti) calculator on farbetteroff.com",
          "url": "https://farbetteroff.com/calculators/mortgage-calculator"
        },
        "responses": {
          "200": {
            "$ref": "#/components/responses/mortgage-result"
          },
          "400": {
            "description": "A parameter is missing, mistyped, out of range or unknown — all of them are listed at once. A name that is one or two edits from a real parameter is answered with the one it probably meant; an ambiguous one is answered with the accepted list instead of a guess.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such endpoint. The response lists every valid slug.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "More than 60 requests in 60 seconds from one IP.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "A bug on our side. The inputs are never at fault for this one.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "operationId": "post-mortgage",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "homePrice",
                  "rate"
                ],
                "properties": {
                  "homePrice": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 10000000000,
                    "x-unit": "usd",
                    "examples": [
                      400000
                    ],
                    "description": "Purchase price. Accepts number, US dollars, between 0 and 10000000000."
                  },
                  "downPayment": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 10000000000,
                    "default": 0,
                    "x-unit": "usd",
                    "examples": [
                      80000
                    ],
                    "description": "Down payment in dollars. Accepts number, US dollars, between 0 and 10000000000."
                  },
                  "rate": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 100,
                    "x-unit": "percent",
                    "examples": [
                      6.5
                    ],
                    "description": "Annual interest rate (APR). Accepts number, a percentage, so 6.5 means 6.5%, between 0 and 100."
                  },
                  "termYears": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 50,
                    "default": 30,
                    "x-unit": "years",
                    "examples": [
                      30
                    ],
                    "description": "Loan term in years. Accepts integer, years, between 1 and 50."
                  },
                  "propertyTaxAnnual": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 10000000,
                    "default": 0,
                    "x-unit": "usd",
                    "examples": [
                      3600
                    ],
                    "description": "Property tax per year. Accepts number, US dollars, between 0 and 10000000."
                  },
                  "insuranceAnnual": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 10000000,
                    "default": 0,
                    "x-unit": "usd",
                    "examples": [
                      1800
                    ],
                    "description": "Homeowner's insurance per year. Accepts number, US dollars, between 0 and 10000000."
                  },
                  "hoaMonthly": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 1000000,
                    "default": 0,
                    "x-unit": "usd",
                    "description": "HOA dues per month. Accepts number, US dollars, between 0 and 1000000."
                  },
                  "pmiRate": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 5,
                    "default": 0.5,
                    "x-unit": "percent",
                    "description": "Annual PMI premium as a percentage of the loan. Only charged below 20% down. Freddie Mac puts a typical premium at $30–$70 a month per $100,000 borrowed, which is 0.36%–0.84% of the loan a year. Accepts number, a percentage, so 6.5 means 6.5%, between 0 and 5."
                  },
                  "extraMonthly": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 10000000,
                    "default": 0,
                    "x-unit": "usd",
                    "description": "Extra principal per month. Accepts number, US dollars, between 0 and 10000000."
                  }
                }
              }
            }
          }
        }
      }
    },
    "/house-affordability": {
      "get": {
        "tags": [
          "calculators"
        ],
        "summary": "How much house can I afford",
        "description": "The highest home price whose whole payment fits inside a lender's two debt-to-income ratios — answered at the 28/36 convention and at both of FHA's manual-underwriting thresholds.\n\nThe price is found by bisection, not by a formula: property tax and the insurance premium both move with the price being tested, so the price appears on both sides of the constraint. Forty halvings of the range between the down payment and $5,000,000 settle it to well under a cent.\n\nThe 28/36 pair is a lending convention with no regulation behind it. FHA's 31/43 is a real qualifying ratio for manually underwritten loans — above it the lender must justify in writing why the mortgage is an acceptable risk — and 37/47 needs a documented compensating factor and a decision credit score of 580 or better (HUD Handbook 4000.1 II.A.5.d).\n\n**PMI is in this payment, and it is inside the ratios rather than on top of them.** That is where underwriting puts it: Fannie Mae's monthly housing expense — the numerator of the debt-to-income ratio these benchmarks are about — is principal and interest plus \"property, flood, and mortgage insurance premiums (as applicable)\" (Selling Guide B3-6-03). So a loan above 80% LTV buys less house, not the same house plus a premium: on `income=90000&monthlyDebts=500&downPayment=40000&rate=6.5` the premium is about $107 a month and costs about $14,858 of price. `pmiApplies` says whether it was charged, `monthlyPmi` is the figure, `pmiRatePct=0` turns it off, and /api/v1/pmi dates its removal. HOA dues, ground rent and closing costs are still not in it, and FHA's mortgage insurance — charged at any down payment, often for the life of the loan — is not modelled.\n\nThe default property tax rate is 0.89% — the national average effective rate, computed from the US Census Bureau's 2024 American Community Survey, https://data.census.gov/table/ACSDT5Y2024.B25103 — and it is an average across counties that range from about 0.3% to over 2%. Send your own `propertyTaxRatePct` if you know it; it moves the answer more than anything here except the rate.\n\nThe default insurance premium is measured rather than assumed, and it is not a flat rate: read off the 2024 American Community Survey 1-year PUMS microdata, where the same household reports both its premium (INSP) and its property value (VALP), the premium rises as roughly the 0.31 power of the price — $1,930 a year at a $400,000 home, about $2,400 at $836,000. A premium is priced on what it costs to rebuild the house, and land cannot burn down. Source: https://www.census.gov/programs-surveys/acs/microdata.html\n\nThis is what a lender would lend, which is not the same as what is comfortable to spend. The ratios say nothing about childcare, retirement saving or the cost of a roof.\n\nSame math as the calculator page: both call `houseAffordability` in @calcwise/finance, so this answer and https://farbetteroff.com/calculators/how-much-house-can-i-afford cannot disagree.",
        "externalDocs": {
          "description": "The how much house can i afford calculator on farbetteroff.com",
          "url": "https://farbetteroff.com/calculators/how-much-house-can-i-afford"
        },
        "responses": {
          "200": {
            "$ref": "#/components/responses/house-affordability-result"
          },
          "400": {
            "description": "A parameter is missing, mistyped, out of range or unknown — all of them are listed at once. A name that is one or two edits from a real parameter is answered with the one it probably meant; an ambiguous one is answered with the accepted list instead of a guess.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such endpoint. The response lists every valid slug.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "More than 60 requests in 60 seconds from one IP.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "A bug on our side. The inputs are never at fault for this one.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "operationId": "get-house-affordability",
        "parameters": [
          {
            "name": "income",
            "in": "query",
            "required": true,
            "description": "Gross household income per year, before tax. Annual, not monthly — the ratios below are applied to a twelfth of it. Accepts number, US dollars, between 1 and 100000000.",
            "schema": {
              "type": "number",
              "minimum": 1,
              "maximum": 100000000,
              "x-unit": "usd",
              "examples": [
                90000
              ]
            }
          },
          {
            "name": "monthlyDebts",
            "in": "query",
            "required": false,
            "description": "Required monthly payments on everything that is not housing — car, student loans, credit-card minimums, alimony and child support. Groceries, utilities and phone bills are not debt and do not belong here. Accepts number, US dollars, between 0 and 1000000.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 1000000,
              "default": 0,
              "x-unit": "usd",
              "examples": [
                500
              ]
            }
          },
          {
            "name": "downPayment",
            "in": "query",
            "required": false,
            "description": "Cash going in. It raises the price you can reach and is not borrowed. Accepts number, US dollars, between 0 and 100000000.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 100000000,
              "default": 0,
              "x-unit": "usd",
              "examples": [
                40000
              ]
            }
          },
          {
            "name": "rate",
            "in": "query",
            "required": true,
            "description": "Mortgage APR you expect to be offered. Accepts number, a percentage, so 6.5 means 6.5%, between 0 and 100.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 100,
              "x-unit": "percent",
              "examples": [
                6.5
              ]
            }
          },
          {
            "name": "termYears",
            "in": "query",
            "required": false,
            "description": "Loan term in years. Accepts integer, years, between 1 and 50.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 50,
              "default": 30,
              "x-unit": "years"
            }
          },
          {
            "name": "propertyTaxRatePct",
            "in": "query",
            "required": false,
            "description": "Annual property tax as a percent of the home's price. Defaults to the US average effective rate; county rates run from about 0.3% to over 2%, so last year's bill divided by the price is the number to send. Accepts number, a percentage, so 6.5 means 6.5%, between 0 and 10.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 10,
              "default": 0.89,
              "x-unit": "percent"
            }
          },
          {
            "name": "insuranceAnnual",
            "in": "query",
            "required": false,
            "description": "Your own annual homeowners premium, if you have a quote. Leave it out and the premium is read off the measured curve described in the notes, which re-prices at every home price the search tries. Accepts number, US dollars, between 0 and 1000000.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 1000000,
              "x-unit": "usd"
            }
          },
          {
            "name": "pmiRatePct",
            "in": "query",
            "required": false,
            "description": "Annual PMI as a percent of the loan, charged only where the answer lands above 80% loan-to-value. Defaults to 0.5%, mid-range for the $30–$70 a month per $100,000 borrowed Freddie Mac describes. Send `pmiRatePct=0` for a payment with no mortgage insurance in it — the right call for a VA loan, or where you are solving the lender's PITI rather than what underwriting measures. Accepts number, a percentage, so 6.5 means 6.5%, between 0 and 5.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 5,
              "default": 0.5,
              "x-unit": "percent"
            }
          }
        ]
      },
      "post": {
        "tags": [
          "calculators"
        ],
        "summary": "How much house can I afford",
        "description": "The highest home price whose whole payment fits inside a lender's two debt-to-income ratios — answered at the 28/36 convention and at both of FHA's manual-underwriting thresholds.\n\nThe price is found by bisection, not by a formula: property tax and the insurance premium both move with the price being tested, so the price appears on both sides of the constraint. Forty halvings of the range between the down payment and $5,000,000 settle it to well under a cent.\n\nThe 28/36 pair is a lending convention with no regulation behind it. FHA's 31/43 is a real qualifying ratio for manually underwritten loans — above it the lender must justify in writing why the mortgage is an acceptable risk — and 37/47 needs a documented compensating factor and a decision credit score of 580 or better (HUD Handbook 4000.1 II.A.5.d).\n\n**PMI is in this payment, and it is inside the ratios rather than on top of them.** That is where underwriting puts it: Fannie Mae's monthly housing expense — the numerator of the debt-to-income ratio these benchmarks are about — is principal and interest plus \"property, flood, and mortgage insurance premiums (as applicable)\" (Selling Guide B3-6-03). So a loan above 80% LTV buys less house, not the same house plus a premium: on `income=90000&monthlyDebts=500&downPayment=40000&rate=6.5` the premium is about $107 a month and costs about $14,858 of price. `pmiApplies` says whether it was charged, `monthlyPmi` is the figure, `pmiRatePct=0` turns it off, and /api/v1/pmi dates its removal. HOA dues, ground rent and closing costs are still not in it, and FHA's mortgage insurance — charged at any down payment, often for the life of the loan — is not modelled.\n\nThe default property tax rate is 0.89% — the national average effective rate, computed from the US Census Bureau's 2024 American Community Survey, https://data.census.gov/table/ACSDT5Y2024.B25103 — and it is an average across counties that range from about 0.3% to over 2%. Send your own `propertyTaxRatePct` if you know it; it moves the answer more than anything here except the rate.\n\nThe default insurance premium is measured rather than assumed, and it is not a flat rate: read off the 2024 American Community Survey 1-year PUMS microdata, where the same household reports both its premium (INSP) and its property value (VALP), the premium rises as roughly the 0.31 power of the price — $1,930 a year at a $400,000 home, about $2,400 at $836,000. A premium is priced on what it costs to rebuild the house, and land cannot burn down. Source: https://www.census.gov/programs-surveys/acs/microdata.html\n\nThis is what a lender would lend, which is not the same as what is comfortable to spend. The ratios say nothing about childcare, retirement saving or the cost of a roof.\n\nSame math as the calculator page: both call `houseAffordability` in @calcwise/finance, so this answer and https://farbetteroff.com/calculators/how-much-house-can-i-afford cannot disagree.\n\nIdentical to the GET, with the parameters in a JSON object body.",
        "externalDocs": {
          "description": "The how much house can i afford calculator on farbetteroff.com",
          "url": "https://farbetteroff.com/calculators/how-much-house-can-i-afford"
        },
        "responses": {
          "200": {
            "$ref": "#/components/responses/house-affordability-result"
          },
          "400": {
            "description": "A parameter is missing, mistyped, out of range or unknown — all of them are listed at once. A name that is one or two edits from a real parameter is answered with the one it probably meant; an ambiguous one is answered with the accepted list instead of a guess.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such endpoint. The response lists every valid slug.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "More than 60 requests in 60 seconds from one IP.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "A bug on our side. The inputs are never at fault for this one.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "operationId": "post-house-affordability",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "income",
                  "rate"
                ],
                "properties": {
                  "income": {
                    "type": "number",
                    "minimum": 1,
                    "maximum": 100000000,
                    "x-unit": "usd",
                    "examples": [
                      90000
                    ],
                    "description": "Gross household income per year, before tax. Annual, not monthly — the ratios below are applied to a twelfth of it. Accepts number, US dollars, between 1 and 100000000."
                  },
                  "monthlyDebts": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 1000000,
                    "default": 0,
                    "x-unit": "usd",
                    "examples": [
                      500
                    ],
                    "description": "Required monthly payments on everything that is not housing — car, student loans, credit-card minimums, alimony and child support. Groceries, utilities and phone bills are not debt and do not belong here. Accepts number, US dollars, between 0 and 1000000."
                  },
                  "downPayment": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 100000000,
                    "default": 0,
                    "x-unit": "usd",
                    "examples": [
                      40000
                    ],
                    "description": "Cash going in. It raises the price you can reach and is not borrowed. Accepts number, US dollars, between 0 and 100000000."
                  },
                  "rate": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 100,
                    "x-unit": "percent",
                    "examples": [
                      6.5
                    ],
                    "description": "Mortgage APR you expect to be offered. Accepts number, a percentage, so 6.5 means 6.5%, between 0 and 100."
                  },
                  "termYears": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 50,
                    "default": 30,
                    "x-unit": "years",
                    "description": "Loan term in years. Accepts integer, years, between 1 and 50."
                  },
                  "propertyTaxRatePct": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 10,
                    "default": 0.89,
                    "x-unit": "percent",
                    "description": "Annual property tax as a percent of the home's price. Defaults to the US average effective rate; county rates run from about 0.3% to over 2%, so last year's bill divided by the price is the number to send. Accepts number, a percentage, so 6.5 means 6.5%, between 0 and 10."
                  },
                  "insuranceAnnual": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 1000000,
                    "x-unit": "usd",
                    "description": "Your own annual homeowners premium, if you have a quote. Leave it out and the premium is read off the measured curve described in the notes, which re-prices at every home price the search tries. Accepts number, US dollars, between 0 and 1000000."
                  },
                  "pmiRatePct": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 5,
                    "default": 0.5,
                    "x-unit": "percent",
                    "description": "Annual PMI as a percent of the loan, charged only where the answer lands above 80% loan-to-value. Defaults to 0.5%, mid-range for the $30–$70 a month per $100,000 borrowed Freddie Mac describes. Send `pmiRatePct=0` for a payment with no mortgage insurance in it — the right call for a VA loan, or where you are solving the lender's PITI rather than what underwriting measures. Accepts number, a percentage, so 6.5 means 6.5%, between 0 and 5."
                  }
                }
              }
            }
          }
        }
      }
    },
    "/rent-affordability": {
      "get": {
        "tags": [
          "calculators"
        ],
        "summary": "How much rent can I afford",
        "description": "The rent a household can carry on the 30% rule and on a lender's back-end ratio, plus where the answer falls on HUD's cost-burden scale — which counts utilities, and almost nothing else does.\n\n**The 30% rule is a convention, not a rule anyone set.** No agency caps what an unassisted renter may pay. The number is borrowed from federal housing policy, where it means two other things — the two paragraphs below — and this endpoint returns all three rather than picking one.\n\n**HUD's 30% counts utilities, and that is the correction most rent calculators need.** 24 CFR § 91.5 defines cost burden as the extent to which “gross housing costs, including utility costs, exceed 30 percent of gross income”, and severe cost burden as the same above 50 percent. A household paying exactly 30% of gross in rent is already cost burdened once the electric bill is counted, which is why `monthlyUtilities` exists and why `costBurdenRent` is the threshold **less** that bill. https://www.ecfr.gov/current/title-24/subtitle-A/part-91/section-91.5\n\n**The other federal 30% is of adjusted income, with a 10%-of-gross floor underneath it.** An assisted household's total tenant payment is the highest of “30 percent of the family's monthly adjusted income” and “10 percent of the family's monthly income” (24 CFR § 5.628(a)), among other amounts. Adjusted income is annual income less the deductions in § 5.611 — dependents, elderly or disabled status, medical costs above 10% of income, and childcare — so it is below gross for any household with children or care costs. Send `dependents`, `childcareAnnual`, `elderlyOrDisabled` and `medicalAnnual` and `federalFormula` computes it. https://www.ecfr.gov/current/title-24/subtitle-A/part-5/subpart-F/section-5.628\n\n**The § 5.611 deduction amounts are the regulation's figures, and HUD raises both every year.** $480 per dependent and $525 for an elderly or disabled family are what the CFR prints; each “will be adjusted by HUD annually in accordance with the Consumer Price Index for Urban Wage Earners and Clerical Workers, rounded to the next lowest multiple of $25”. This API does not claim a current-year adjusted amount, so those two are floors — which makes `federalFormula.totalTenantPayment` at or slightly **above** what a housing authority would charge, the safe direction to be wrong in for anyone planning a budget.\n\nThe back-end ceiling is a mortgage ratio applied to rent, and it is labelled as one. 36 is the back-end half of the 28/36 convention; 43 and 47 are FHA's manual-underwriting thresholds, where above 31%/43% the lender must justify in writing why the loan is an acceptable risk and 37%/47% additionally needs a documented compensating factor and a decision credit score of 580 (HUD Handbook 4000.1 II.A.5.d). No regulator sets a back-end ratio for a lease.\n\nThe 3x-income screen most landlords apply is the same arithmetic as a 33% share, so `landlordMaxRent` will sit slightly above the 30% answer. It is a screening rule private landlords choose, not law, and it varies by market.\n\nSecurity deposits, first and last month up front, renters insurance, parking and commuting are not in any of these numbers.\n\nSame math as the calculator page: both call `rentAffordability` in @calcwise/finance, so this answer and https://farbetteroff.com/calculators/rent-affordability-calculator cannot disagree.",
        "externalDocs": {
          "description": "The how much rent can i afford calculator on farbetteroff.com",
          "url": "https://farbetteroff.com/calculators/rent-affordability-calculator"
        },
        "responses": {
          "200": {
            "$ref": "#/components/responses/rent-affordability-result"
          },
          "400": {
            "description": "A parameter is missing, mistyped, out of range or unknown — all of them are listed at once. A name that is one or two edits from a real parameter is answered with the one it probably meant; an ambiguous one is answered with the accepted list instead of a guess.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such endpoint. The response lists every valid slug.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "More than 60 requests in 60 seconds from one IP.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "A bug on our side. The inputs are never at fault for this one.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "operationId": "get-rent-affordability",
        "parameters": [
          {
            "name": "income",
            "in": "query",
            "required": true,
            "description": "Gross household income per year, before tax. Annual, not monthly. Gross rather than take-home, because every rule here is written on gross — the landlord's 3x test included. Accepts number, US dollars, between 1 and 100000000.",
            "schema": {
              "type": "number",
              "minimum": 1,
              "maximum": 100000000,
              "x-unit": "usd",
              "examples": [
                60000
              ]
            }
          },
          {
            "name": "monthlyDebts",
            "in": "query",
            "required": false,
            "description": "Required monthly payments on everything that is not housing — car, student loans, credit-card minimums, alimony and child support. Groceries, phone and utilities are not debt; utilities have their own parameter below. Accepts number, US dollars, between 0 and 1000000.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 1000000,
              "default": 0,
              "x-unit": "usd",
              "examples": [
                600
              ]
            }
          },
          {
            "name": "monthlyUtilities",
            "in": "query",
            "required": false,
            "description": "Electric, gas, water and trash you pay on top of the rent. HUD counts these inside housing cost, so they move `costBurdenRent` and `burdenAtMaxRent` — send them or those two answers are optimistic. Accepts number, US dollars, between 0 and 100000.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 100000,
              "default": 0,
              "x-unit": "usd",
              "examples": [
                180
              ]
            }
          },
          {
            "name": "sharePct",
            "in": "query",
            "required": false,
            "description": "Share of gross income to target for rent. Defaults to the 30% convention, which is a rule of thumb nobody set — see the notes. Accepts number, a percentage, so 6.5 means 6.5%, between 1 and 100.",
            "schema": {
              "type": "number",
              "minimum": 1,
              "maximum": 100,
              "default": 30,
              "x-unit": "percent"
            }
          },
          {
            "name": "backEndPct",
            "in": "query",
            "required": false,
            "description": "Back-end debt-to-income ratio used as the second ceiling on rent plus all other debt. Defaults to 36, the back-end half of the 28/36 lending convention. `byBenchmark` answers at every benchmark in the table regardless of what is sent here. Accepts number, a percentage, so 6.5 means 6.5%, between 1 and 100.",
            "schema": {
              "type": "number",
              "minimum": 1,
              "maximum": 100,
              "default": 36,
              "x-unit": "percent"
            }
          },
          {
            "name": "dependents",
            "in": "query",
            "required": false,
            "description": "Number of dependents. Used only by `federalFormula`, where each one is a deduction under 24 CFR § 5.611(a)(1). It does not change `maxRent` — no budgeting rule of thumb accounts for household size. Accepts integer, between 0 and 20.",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "maximum": 20,
              "default": 0
            }
          },
          {
            "name": "childcareAnnual",
            "in": "query",
            "required": false,
            "description": "Reasonable childcare per year needed to work or study. Deductible in full under 24 CFR § 5.611(a)(4), so it enters `federalFormula` only. Accepts number, US dollars, between 0 and 1000000.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 1000000,
              "default": 0,
              "x-unit": "usd"
            }
          },
          {
            "name": "elderlyOrDisabled",
            "in": "query",
            "required": false,
            "description": "True for an elderly or disabled family, which unlocks the § 5.611(a)(2) deduction and the medical deduction below. `federalFormula` only. Accepts boolean.",
            "schema": {
              "type": "boolean",
              "default": false
            }
          },
          {
            "name": "medicalAnnual",
            "in": "query",
            "required": false,
            "description": "Unreimbursed health, medical and attendant-care costs per year. Deductible only for an elderly or disabled family, and only above 10 percent of annual income. `federalFormula` only. Accepts number, US dollars, between 0 and 1000000.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 1000000,
              "default": 0,
              "x-unit": "usd"
            }
          }
        ]
      },
      "post": {
        "tags": [
          "calculators"
        ],
        "summary": "How much rent can I afford",
        "description": "The rent a household can carry on the 30% rule and on a lender's back-end ratio, plus where the answer falls on HUD's cost-burden scale — which counts utilities, and almost nothing else does.\n\n**The 30% rule is a convention, not a rule anyone set.** No agency caps what an unassisted renter may pay. The number is borrowed from federal housing policy, where it means two other things — the two paragraphs below — and this endpoint returns all three rather than picking one.\n\n**HUD's 30% counts utilities, and that is the correction most rent calculators need.** 24 CFR § 91.5 defines cost burden as the extent to which “gross housing costs, including utility costs, exceed 30 percent of gross income”, and severe cost burden as the same above 50 percent. A household paying exactly 30% of gross in rent is already cost burdened once the electric bill is counted, which is why `monthlyUtilities` exists and why `costBurdenRent` is the threshold **less** that bill. https://www.ecfr.gov/current/title-24/subtitle-A/part-91/section-91.5\n\n**The other federal 30% is of adjusted income, with a 10%-of-gross floor underneath it.** An assisted household's total tenant payment is the highest of “30 percent of the family's monthly adjusted income” and “10 percent of the family's monthly income” (24 CFR § 5.628(a)), among other amounts. Adjusted income is annual income less the deductions in § 5.611 — dependents, elderly or disabled status, medical costs above 10% of income, and childcare — so it is below gross for any household with children or care costs. Send `dependents`, `childcareAnnual`, `elderlyOrDisabled` and `medicalAnnual` and `federalFormula` computes it. https://www.ecfr.gov/current/title-24/subtitle-A/part-5/subpart-F/section-5.628\n\n**The § 5.611 deduction amounts are the regulation's figures, and HUD raises both every year.** $480 per dependent and $525 for an elderly or disabled family are what the CFR prints; each “will be adjusted by HUD annually in accordance with the Consumer Price Index for Urban Wage Earners and Clerical Workers, rounded to the next lowest multiple of $25”. This API does not claim a current-year adjusted amount, so those two are floors — which makes `federalFormula.totalTenantPayment` at or slightly **above** what a housing authority would charge, the safe direction to be wrong in for anyone planning a budget.\n\nThe back-end ceiling is a mortgage ratio applied to rent, and it is labelled as one. 36 is the back-end half of the 28/36 convention; 43 and 47 are FHA's manual-underwriting thresholds, where above 31%/43% the lender must justify in writing why the loan is an acceptable risk and 37%/47% additionally needs a documented compensating factor and a decision credit score of 580 (HUD Handbook 4000.1 II.A.5.d). No regulator sets a back-end ratio for a lease.\n\nThe 3x-income screen most landlords apply is the same arithmetic as a 33% share, so `landlordMaxRent` will sit slightly above the 30% answer. It is a screening rule private landlords choose, not law, and it varies by market.\n\nSecurity deposits, first and last month up front, renters insurance, parking and commuting are not in any of these numbers.\n\nSame math as the calculator page: both call `rentAffordability` in @calcwise/finance, so this answer and https://farbetteroff.com/calculators/rent-affordability-calculator cannot disagree.\n\nIdentical to the GET, with the parameters in a JSON object body.",
        "externalDocs": {
          "description": "The how much rent can i afford calculator on farbetteroff.com",
          "url": "https://farbetteroff.com/calculators/rent-affordability-calculator"
        },
        "responses": {
          "200": {
            "$ref": "#/components/responses/rent-affordability-result"
          },
          "400": {
            "description": "A parameter is missing, mistyped, out of range or unknown — all of them are listed at once. A name that is one or two edits from a real parameter is answered with the one it probably meant; an ambiguous one is answered with the accepted list instead of a guess.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such endpoint. The response lists every valid slug.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "More than 60 requests in 60 seconds from one IP.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "A bug on our side. The inputs are never at fault for this one.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "operationId": "post-rent-affordability",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "income"
                ],
                "properties": {
                  "income": {
                    "type": "number",
                    "minimum": 1,
                    "maximum": 100000000,
                    "x-unit": "usd",
                    "examples": [
                      60000
                    ],
                    "description": "Gross household income per year, before tax. Annual, not monthly. Gross rather than take-home, because every rule here is written on gross — the landlord's 3x test included. Accepts number, US dollars, between 1 and 100000000."
                  },
                  "monthlyDebts": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 1000000,
                    "default": 0,
                    "x-unit": "usd",
                    "examples": [
                      600
                    ],
                    "description": "Required monthly payments on everything that is not housing — car, student loans, credit-card minimums, alimony and child support. Groceries, phone and utilities are not debt; utilities have their own parameter below. Accepts number, US dollars, between 0 and 1000000."
                  },
                  "monthlyUtilities": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 100000,
                    "default": 0,
                    "x-unit": "usd",
                    "examples": [
                      180
                    ],
                    "description": "Electric, gas, water and trash you pay on top of the rent. HUD counts these inside housing cost, so they move `costBurdenRent` and `burdenAtMaxRent` — send them or those two answers are optimistic. Accepts number, US dollars, between 0 and 100000."
                  },
                  "sharePct": {
                    "type": "number",
                    "minimum": 1,
                    "maximum": 100,
                    "default": 30,
                    "x-unit": "percent",
                    "description": "Share of gross income to target for rent. Defaults to the 30% convention, which is a rule of thumb nobody set — see the notes. Accepts number, a percentage, so 6.5 means 6.5%, between 1 and 100."
                  },
                  "backEndPct": {
                    "type": "number",
                    "minimum": 1,
                    "maximum": 100,
                    "default": 36,
                    "x-unit": "percent",
                    "description": "Back-end debt-to-income ratio used as the second ceiling on rent plus all other debt. Defaults to 36, the back-end half of the 28/36 lending convention. `byBenchmark` answers at every benchmark in the table regardless of what is sent here. Accepts number, a percentage, so 6.5 means 6.5%, between 1 and 100."
                  },
                  "dependents": {
                    "type": "integer",
                    "minimum": 0,
                    "maximum": 20,
                    "default": 0,
                    "description": "Number of dependents. Used only by `federalFormula`, where each one is a deduction under 24 CFR § 5.611(a)(1). It does not change `maxRent` — no budgeting rule of thumb accounts for household size. Accepts integer, between 0 and 20."
                  },
                  "childcareAnnual": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 1000000,
                    "default": 0,
                    "x-unit": "usd",
                    "description": "Reasonable childcare per year needed to work or study. Deductible in full under 24 CFR § 5.611(a)(4), so it enters `federalFormula` only. Accepts number, US dollars, between 0 and 1000000."
                  },
                  "elderlyOrDisabled": {
                    "type": "boolean",
                    "default": false,
                    "description": "True for an elderly or disabled family, which unlocks the § 5.611(a)(2) deduction and the medical deduction below. `federalFormula` only. Accepts boolean."
                  },
                  "medicalAnnual": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 1000000,
                    "default": 0,
                    "x-unit": "usd",
                    "description": "Unreimbursed health, medical and attendant-care costs per year. Deductible only for an elderly or disabled family, and only above 10 percent of annual income. `federalFormula` only. Accepts number, US dollars, between 0 and 1000000."
                  }
                }
              }
            }
          }
        }
      }
    },
    "/car-affordability": {
      "get": {
        "tags": [
          "calculators"
        ],
        "summary": "How much car can I afford",
        "description": "The car the 20/4/10 rule leaves room for, with insurance, fuel and upkeep taken out of the ten percent first — from the IRS's own operating allowance for the caller's part of the country, beside what the IRS allows for the payment itself.\n\n**The ten percent is all car costs, not the payment.** The rule caps the payment together with insurance, fuel, maintenance and registration at 10% of take-home, so this endpoint subtracts running costs from the allowance before solving for a loan. Calculators that solve 10% straight into a payment answer a much looser question — on $4,200 of take-home, $420 a month of payment instead of $124.\n\n**Running costs default to the IRS's own allowance.** The Collection Financial Standards split what a car costs exactly the way this calculation needs: ownership costs of $703 a month for the loan or lease, and operating costs by census region and metropolitan area covering “maintenance, repairs, insurance, fuel, registrations, licenses, inspections, parking and tolls” — $248 in Anchorage to $423 in Miami. Send `area` to pick one. https://www.irs.gov/businesses/small-businesses-self-employed/local-standards-transportation\n\n**Those allowances are not advice, and the endpoint does not present them as any.** They are what the IRS treats as a necessary living expense when calculating repayment of delinquent taxes — a ceiling on what a taxpayer may claim, derived from Bureau of Labor Statistics expenditure data. A household spending more than the standard is ordinary. They also exclude personal property taxes and say nothing about depreciation.\n\n**The national average is arithmetic on the table, not a published figure.** The IRS prints no nationwide operating cost, so a caller who names no `area` gets the unweighted mean of the four census-region allowances — $295.75. It is the middle of four government figures, not a population-weighted estimate of what households pay, and `operatingCostBasis` says so on every response that uses it.\n\n**20/4/10 is a convention with no authority behind it.** No agency publishes or enforces it. Worth holding beside `irsAllowance`: the IRS treats about $1,000 a month as necessary for one car, while the rule allows $420 on $4,200 of take-home. Neither is a law; they are two different answers, and this endpoint returns both rather than picking.\n\nThe first leg is reported, not applied. Enforcing “20% down” would answer $0 for anyone with nothing saved, which is what the rule literally says and is useless as a ceiling to shop against — so `maxPrice` is the payment answer and `twentyPctDown` tells you where you stand.\n\n`maxPrice` is the car, not the drive-away cost: sales tax, title, registration and dealer fees are on top, and this endpoint does not model them.\n\nSame math as the calculator page: both call `carAffordability` in @calcwise/finance, so this answer and https://farbetteroff.com/calculators/car-affordability-calculator cannot disagree.",
        "externalDocs": {
          "description": "The how much car can i afford calculator on farbetteroff.com",
          "url": "https://farbetteroff.com/calculators/car-affordability-calculator"
        },
        "responses": {
          "200": {
            "$ref": "#/components/responses/car-affordability-result"
          },
          "400": {
            "description": "A parameter is missing, mistyped, out of range or unknown — all of them are listed at once. A name that is one or two edits from a real parameter is answered with the one it probably meant; an ambiguous one is answered with the accepted list instead of a guess.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such endpoint. The response lists every valid slug.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "More than 60 requests in 60 seconds from one IP.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "A bug on our side. The inputs are never at fault for this one.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "operationId": "get-car-affordability",
        "parameters": [
          {
            "name": "takeHome",
            "in": "query",
            "required": true,
            "description": "Monthly pay after tax. Take-home rather than gross, because the rule is written on take-home — sending gross overstates every answer here by roughly a quarter. Accepts number, US dollars, between 1 and 10000000.",
            "schema": {
              "type": "number",
              "minimum": 1,
              "maximum": 10000000,
              "x-unit": "usd",
              "examples": [
                4200
              ]
            }
          },
          {
            "name": "downPayment",
            "in": "query",
            "required": false,
            "description": "Down payment plus trade-in. It adds to the price directly and is what the rule's 20% leg is measured against. Accepts number, US dollars, between 0 and 10000000.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 10000000,
              "default": 0,
              "x-unit": "usd",
              "examples": [
                4000
              ]
            }
          },
          {
            "name": "rate",
            "in": "query",
            "required": true,
            "description": "Annual percentage rate on the car loan. Accepts number, a percentage, so 6.5 means 6.5%, between 0 and 100.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 100,
              "x-unit": "percent",
              "examples": [
                7.5
              ]
            }
          },
          {
            "name": "termMonths",
            "in": "query",
            "required": false,
            "description": "Loan term in months. Defaults to the rule's 48 — `byTerm` prices 36, 48, 60 and 72 at the same monthly payment regardless, which is the cheapest way to see what stretching costs. Accepts integer, whole months, between 1 and 120.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 120,
              "default": 48,
              "x-unit": "months"
            }
          },
          {
            "name": "sharePct",
            "in": "query",
            "required": false,
            "description": "Share of take-home pay for **all** car costs, payment and running costs together. Defaults to the rule's 10, which is a convention nobody set — see the notes. Accepts number, a percentage, so 6.5 means 6.5%, between 1 and 100.",
            "schema": {
              "type": "number",
              "minimum": 1,
              "maximum": 100,
              "default": 10,
              "x-unit": "percent"
            }
          },
          {
            "name": "area",
            "in": "query",
            "required": false,
            "description": "Where the car is run, as an IRS area id (\"miami\", \"west\"), the label the IRS itself prints (\"Washington, D.C.\", \"West region\") or a USPS state code (\"tx\" or \"TX\", either case, which resolves to its census region). Picks the IRS operating allowance used for insurance, fuel and upkeep. Omit it and the four regions' mean is used instead, flagged as `national-average`. Accepts one of northeast, boston, new-york, philadelphia, midwest, chicago, cleveland, detroit, minneapolis-st-paul, st-louis, south, atlanta, baltimore, dallas-ft-worth, houston, miami, tampa, washington-dc, west, anchorage, denver, honolulu, los-angeles, phoenix, san-diego, san-francisco, seattle. 59 of the listed spellings are aliases: they are resolved to one of 27 canonical values (`x-canonical-values`) before the answer echoes them. Matching ignores case.",
            "schema": {
              "type": "string",
              "enum": [
                "northeast",
                "boston",
                "new-york",
                "philadelphia",
                "midwest",
                "chicago",
                "cleveland",
                "detroit",
                "minneapolis-st-paul",
                "st-louis",
                "south",
                "atlanta",
                "baltimore",
                "dallas-ft-worth",
                "houston",
                "miami",
                "tampa",
                "washington-dc",
                "west",
                "anchorage",
                "denver",
                "honolulu",
                "los-angeles",
                "phoenix",
                "san-diego",
                "san-francisco",
                "seattle",
                "northeast region",
                "midwest region",
                "minneapolis-st. paul",
                "st. louis",
                "south region",
                "dallas-ft. worth",
                "washington, d.c.",
                "west region",
                "me",
                "nh",
                "vt",
                "ma",
                "ri",
                "ct",
                "pa",
                "ny",
                "nj",
                "nd",
                "sd",
                "ne",
                "ks",
                "mo",
                "il",
                "in",
                "oh",
                "mi",
                "wi",
                "mn",
                "ia",
                "tx",
                "ok",
                "ar",
                "la",
                "ms",
                "tn",
                "ky",
                "wv",
                "va",
                "md",
                "dc",
                "de",
                "nc",
                "sc",
                "ga",
                "fl",
                "al",
                "nm",
                "az",
                "co",
                "wy",
                "mt",
                "nv",
                "ut",
                "wa",
                "or",
                "id",
                "ca",
                "ak",
                "hi"
              ],
              "x-canonical-values": [
                "northeast",
                "boston",
                "new-york",
                "philadelphia",
                "midwest",
                "chicago",
                "cleveland",
                "detroit",
                "minneapolis-st-paul",
                "st-louis",
                "south",
                "atlanta",
                "baltimore",
                "dallas-ft-worth",
                "houston",
                "miami",
                "tampa",
                "washington-dc",
                "west",
                "anchorage",
                "denver",
                "honolulu",
                "los-angeles",
                "phoenix",
                "san-diego",
                "san-francisco",
                "seattle"
              ],
              "examples": [
                "tx"
              ]
            }
          },
          {
            "name": "monthlyOperatingCosts",
            "in": "query",
            "required": false,
            "description": "What insurance, fuel and upkeep actually cost you each month. Overrides `area` when sent, and it is the number to send if you know it — the IRS allowance is a ceiling on what a taxpayer may claim, not a measurement of your bills. Accepts number, between 0 and 100000.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 100000
            }
          },
          {
            "name": "cars",
            "in": "query",
            "required": false,
            "description": "How many cars the household runs. Scales both IRS allowances; the table itself only goes to two. Accepts integer, between 1 and 2.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 2,
              "default": 1
            }
          }
        ]
      },
      "post": {
        "tags": [
          "calculators"
        ],
        "summary": "How much car can I afford",
        "description": "The car the 20/4/10 rule leaves room for, with insurance, fuel and upkeep taken out of the ten percent first — from the IRS's own operating allowance for the caller's part of the country, beside what the IRS allows for the payment itself.\n\n**The ten percent is all car costs, not the payment.** The rule caps the payment together with insurance, fuel, maintenance and registration at 10% of take-home, so this endpoint subtracts running costs from the allowance before solving for a loan. Calculators that solve 10% straight into a payment answer a much looser question — on $4,200 of take-home, $420 a month of payment instead of $124.\n\n**Running costs default to the IRS's own allowance.** The Collection Financial Standards split what a car costs exactly the way this calculation needs: ownership costs of $703 a month for the loan or lease, and operating costs by census region and metropolitan area covering “maintenance, repairs, insurance, fuel, registrations, licenses, inspections, parking and tolls” — $248 in Anchorage to $423 in Miami. Send `area` to pick one. https://www.irs.gov/businesses/small-businesses-self-employed/local-standards-transportation\n\n**Those allowances are not advice, and the endpoint does not present them as any.** They are what the IRS treats as a necessary living expense when calculating repayment of delinquent taxes — a ceiling on what a taxpayer may claim, derived from Bureau of Labor Statistics expenditure data. A household spending more than the standard is ordinary. They also exclude personal property taxes and say nothing about depreciation.\n\n**The national average is arithmetic on the table, not a published figure.** The IRS prints no nationwide operating cost, so a caller who names no `area` gets the unweighted mean of the four census-region allowances — $295.75. It is the middle of four government figures, not a population-weighted estimate of what households pay, and `operatingCostBasis` says so on every response that uses it.\n\n**20/4/10 is a convention with no authority behind it.** No agency publishes or enforces it. Worth holding beside `irsAllowance`: the IRS treats about $1,000 a month as necessary for one car, while the rule allows $420 on $4,200 of take-home. Neither is a law; they are two different answers, and this endpoint returns both rather than picking.\n\nThe first leg is reported, not applied. Enforcing “20% down” would answer $0 for anyone with nothing saved, which is what the rule literally says and is useless as a ceiling to shop against — so `maxPrice` is the payment answer and `twentyPctDown` tells you where you stand.\n\n`maxPrice` is the car, not the drive-away cost: sales tax, title, registration and dealer fees are on top, and this endpoint does not model them.\n\nSame math as the calculator page: both call `carAffordability` in @calcwise/finance, so this answer and https://farbetteroff.com/calculators/car-affordability-calculator cannot disagree.\n\nIdentical to the GET, with the parameters in a JSON object body.",
        "externalDocs": {
          "description": "The how much car can i afford calculator on farbetteroff.com",
          "url": "https://farbetteroff.com/calculators/car-affordability-calculator"
        },
        "responses": {
          "200": {
            "$ref": "#/components/responses/car-affordability-result"
          },
          "400": {
            "description": "A parameter is missing, mistyped, out of range or unknown — all of them are listed at once. A name that is one or two edits from a real parameter is answered with the one it probably meant; an ambiguous one is answered with the accepted list instead of a guess.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such endpoint. The response lists every valid slug.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "More than 60 requests in 60 seconds from one IP.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "A bug on our side. The inputs are never at fault for this one.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "operationId": "post-car-affordability",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "takeHome",
                  "rate"
                ],
                "properties": {
                  "takeHome": {
                    "type": "number",
                    "minimum": 1,
                    "maximum": 10000000,
                    "x-unit": "usd",
                    "examples": [
                      4200
                    ],
                    "description": "Monthly pay after tax. Take-home rather than gross, because the rule is written on take-home — sending gross overstates every answer here by roughly a quarter. Accepts number, US dollars, between 1 and 10000000."
                  },
                  "downPayment": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 10000000,
                    "default": 0,
                    "x-unit": "usd",
                    "examples": [
                      4000
                    ],
                    "description": "Down payment plus trade-in. It adds to the price directly and is what the rule's 20% leg is measured against. Accepts number, US dollars, between 0 and 10000000."
                  },
                  "rate": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 100,
                    "x-unit": "percent",
                    "examples": [
                      7.5
                    ],
                    "description": "Annual percentage rate on the car loan. Accepts number, a percentage, so 6.5 means 6.5%, between 0 and 100."
                  },
                  "termMonths": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 120,
                    "default": 48,
                    "x-unit": "months",
                    "description": "Loan term in months. Defaults to the rule's 48 — `byTerm` prices 36, 48, 60 and 72 at the same monthly payment regardless, which is the cheapest way to see what stretching costs. Accepts integer, whole months, between 1 and 120."
                  },
                  "sharePct": {
                    "type": "number",
                    "minimum": 1,
                    "maximum": 100,
                    "default": 10,
                    "x-unit": "percent",
                    "description": "Share of take-home pay for **all** car costs, payment and running costs together. Defaults to the rule's 10, which is a convention nobody set — see the notes. Accepts number, a percentage, so 6.5 means 6.5%, between 1 and 100."
                  },
                  "area": {
                    "type": "string",
                    "enum": [
                      "northeast",
                      "boston",
                      "new-york",
                      "philadelphia",
                      "midwest",
                      "chicago",
                      "cleveland",
                      "detroit",
                      "minneapolis-st-paul",
                      "st-louis",
                      "south",
                      "atlanta",
                      "baltimore",
                      "dallas-ft-worth",
                      "houston",
                      "miami",
                      "tampa",
                      "washington-dc",
                      "west",
                      "anchorage",
                      "denver",
                      "honolulu",
                      "los-angeles",
                      "phoenix",
                      "san-diego",
                      "san-francisco",
                      "seattle",
                      "northeast region",
                      "midwest region",
                      "minneapolis-st. paul",
                      "st. louis",
                      "south region",
                      "dallas-ft. worth",
                      "washington, d.c.",
                      "west region",
                      "me",
                      "nh",
                      "vt",
                      "ma",
                      "ri",
                      "ct",
                      "pa",
                      "ny",
                      "nj",
                      "nd",
                      "sd",
                      "ne",
                      "ks",
                      "mo",
                      "il",
                      "in",
                      "oh",
                      "mi",
                      "wi",
                      "mn",
                      "ia",
                      "tx",
                      "ok",
                      "ar",
                      "la",
                      "ms",
                      "tn",
                      "ky",
                      "wv",
                      "va",
                      "md",
                      "dc",
                      "de",
                      "nc",
                      "sc",
                      "ga",
                      "fl",
                      "al",
                      "nm",
                      "az",
                      "co",
                      "wy",
                      "mt",
                      "nv",
                      "ut",
                      "wa",
                      "or",
                      "id",
                      "ca",
                      "ak",
                      "hi"
                    ],
                    "x-canonical-values": [
                      "northeast",
                      "boston",
                      "new-york",
                      "philadelphia",
                      "midwest",
                      "chicago",
                      "cleveland",
                      "detroit",
                      "minneapolis-st-paul",
                      "st-louis",
                      "south",
                      "atlanta",
                      "baltimore",
                      "dallas-ft-worth",
                      "houston",
                      "miami",
                      "tampa",
                      "washington-dc",
                      "west",
                      "anchorage",
                      "denver",
                      "honolulu",
                      "los-angeles",
                      "phoenix",
                      "san-diego",
                      "san-francisco",
                      "seattle"
                    ],
                    "examples": [
                      "tx"
                    ],
                    "description": "Where the car is run, as an IRS area id (\"miami\", \"west\"), the label the IRS itself prints (\"Washington, D.C.\", \"West region\") or a USPS state code (\"tx\" or \"TX\", either case, which resolves to its census region). Picks the IRS operating allowance used for insurance, fuel and upkeep. Omit it and the four regions' mean is used instead, flagged as `national-average`. Accepts one of northeast, boston, new-york, philadelphia, midwest, chicago, cleveland, detroit, minneapolis-st-paul, st-louis, south, atlanta, baltimore, dallas-ft-worth, houston, miami, tampa, washington-dc, west, anchorage, denver, honolulu, los-angeles, phoenix, san-diego, san-francisco, seattle. 59 of the listed spellings are aliases: they are resolved to one of 27 canonical values (`x-canonical-values`) before the answer echoes them. Matching ignores case."
                  },
                  "monthlyOperatingCosts": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 100000,
                    "description": "What insurance, fuel and upkeep actually cost you each month. Overrides `area` when sent, and it is the number to send if you know it — the IRS allowance is a ceiling on what a taxpayer may claim, not a measurement of your bills. Accepts number, between 0 and 100000."
                  },
                  "cars": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 2,
                    "default": 1,
                    "description": "How many cars the household runs. Scales both IRS allowances; the table itself only goes to two. Accepts integer, between 1 and 2."
                  }
                }
              }
            }
          }
        }
      }
    },
    "/pmi": {
      "get": {
        "tags": [
          "calculators"
        ],
        "summary": "PMI termination schedule",
        "description": "When private mortgage insurance comes off a conventional loan — the automatic date, the earlier date you can ask, and what waiting costs.\n\nSource: CFPB, “When can I remove private mortgage insurance (PMI) from my loan?” — https://www.consumerfinance.gov/ask-cfpb/when-can-i-remove-private-mortgage-insurance-pmi-from-my-loan-en-202/\n\nConventional loans only. FHA mortgage insurance follows different rules and is not modelled.\n\nDates are based on the original amortization schedule, which is what the statute uses — not on a new appraisal.",
        "externalDocs": {
          "description": "The pmi termination schedule calculator on farbetteroff.com",
          "url": "https://farbetteroff.com/calculators/mortgage-calculator"
        },
        "responses": {
          "200": {
            "$ref": "#/components/responses/pmi-result"
          },
          "400": {
            "description": "A parameter is missing, mistyped, out of range or unknown — all of them are listed at once. A name that is one or two edits from a real parameter is answered with the one it probably meant; an ambiguous one is answered with the accepted list instead of a guess.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such endpoint. The response lists every valid slug.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "More than 60 requests in 60 seconds from one IP.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "A bug on our side. The inputs are never at fault for this one.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "operationId": "get-pmi",
        "parameters": [
          {
            "name": "principal",
            "in": "query",
            "required": true,
            "description": "Original loan amount. Accepts number, US dollars, between 1 and 10000000000.",
            "schema": {
              "type": "number",
              "minimum": 1,
              "maximum": 10000000000,
              "x-unit": "usd",
              "examples": [
                190000
              ]
            }
          },
          {
            "name": "rate",
            "in": "query",
            "required": true,
            "description": "Annual interest rate (APR). Accepts number, a percentage, so 6.5 means 6.5%, between 0 and 100.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 100,
              "x-unit": "percent",
              "examples": [
                6.5
              ]
            }
          },
          {
            "name": "termMonths",
            "in": "query",
            "required": true,
            "description": "Loan term in months. Accepts integer, whole months, between 1 and 1200.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 1200,
              "x-unit": "months",
              "examples": [
                360
              ]
            }
          },
          {
            "name": "originalValue",
            "in": "query",
            "required": true,
            "description": "The home's original value — the lower of purchase price and original appraised value, which is the figure the rule is measured against. Accepts number, US dollars, between 1 and 10000000000.",
            "schema": {
              "type": "number",
              "minimum": 1,
              "maximum": 10000000000,
              "x-unit": "usd",
              "examples": [
                200000
              ]
            }
          },
          {
            "name": "pmiMonthly",
            "in": "query",
            "required": true,
            "description": "PMI premium per month. Accepts number, US dollars, between 0 and 100000.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 100000,
              "x-unit": "usd",
              "examples": [
                79
              ]
            }
          },
          {
            "name": "extraMonthly",
            "in": "query",
            "required": false,
            "description": "Extra principal per month, which brings the automatic date forward. Accepts number, US dollars, between 0 and 10000000.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 10000000,
              "default": 0,
              "x-unit": "usd"
            }
          }
        ]
      },
      "post": {
        "tags": [
          "calculators"
        ],
        "summary": "PMI termination schedule",
        "description": "When private mortgage insurance comes off a conventional loan — the automatic date, the earlier date you can ask, and what waiting costs.\n\nSource: CFPB, “When can I remove private mortgage insurance (PMI) from my loan?” — https://www.consumerfinance.gov/ask-cfpb/when-can-i-remove-private-mortgage-insurance-pmi-from-my-loan-en-202/\n\nConventional loans only. FHA mortgage insurance follows different rules and is not modelled.\n\nDates are based on the original amortization schedule, which is what the statute uses — not on a new appraisal.\n\nIdentical to the GET, with the parameters in a JSON object body.",
        "externalDocs": {
          "description": "The pmi termination schedule calculator on farbetteroff.com",
          "url": "https://farbetteroff.com/calculators/mortgage-calculator"
        },
        "responses": {
          "200": {
            "$ref": "#/components/responses/pmi-result"
          },
          "400": {
            "description": "A parameter is missing, mistyped, out of range or unknown — all of them are listed at once. A name that is one or two edits from a real parameter is answered with the one it probably meant; an ambiguous one is answered with the accepted list instead of a guess.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such endpoint. The response lists every valid slug.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "More than 60 requests in 60 seconds from one IP.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "A bug on our side. The inputs are never at fault for this one.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "operationId": "post-pmi",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "principal",
                  "rate",
                  "termMonths",
                  "originalValue",
                  "pmiMonthly"
                ],
                "properties": {
                  "principal": {
                    "type": "number",
                    "minimum": 1,
                    "maximum": 10000000000,
                    "x-unit": "usd",
                    "examples": [
                      190000
                    ],
                    "description": "Original loan amount. Accepts number, US dollars, between 1 and 10000000000."
                  },
                  "rate": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 100,
                    "x-unit": "percent",
                    "examples": [
                      6.5
                    ],
                    "description": "Annual interest rate (APR). Accepts number, a percentage, so 6.5 means 6.5%, between 0 and 100."
                  },
                  "termMonths": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 1200,
                    "x-unit": "months",
                    "examples": [
                      360
                    ],
                    "description": "Loan term in months. Accepts integer, whole months, between 1 and 1200."
                  },
                  "originalValue": {
                    "type": "number",
                    "minimum": 1,
                    "maximum": 10000000000,
                    "x-unit": "usd",
                    "examples": [
                      200000
                    ],
                    "description": "The home's original value — the lower of purchase price and original appraised value, which is the figure the rule is measured against. Accepts number, US dollars, between 1 and 10000000000."
                  },
                  "pmiMonthly": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 100000,
                    "x-unit": "usd",
                    "examples": [
                      79
                    ],
                    "description": "PMI premium per month. Accepts number, US dollars, between 0 and 100000."
                  },
                  "extraMonthly": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 10000000,
                    "default": 0,
                    "x-unit": "usd",
                    "description": "Extra principal per month, which brings the automatic date forward. Accepts number, US dollars, between 0 and 10000000."
                  }
                }
              }
            }
          }
        }
      }
    },
    "/debt-to-income": {
      "get": {
        "tags": [
          "calculators"
        ],
        "summary": "Debt-to-income ratio",
        "description": "Front-end and back-end DTI, residual income, and which of the underwriting benchmarks that actually govern something the result clears.\n\nGross income, never take-home. DTI is computed on income before tax because that is what a lender uses; comparing debts to net pay overstates the ratio.\n\nWhat counts as debt is defined, not guessed: 12 CFR 1026.43(c)(7)(i)(A) sums the mortgage payment, simultaneous loans, mortgage-related obligations (property tax, insurance, HOA) and current debt obligations including alimony and child support. Everyday living costs — groceries, utilities, phone — are not in that list and are not in this ratio.\n\n**43% is no longer the CFPB's qualified-mortgage limit, and citing it as one is out of date.** The CFPB's General QM Final Rule removed the General QM definition's 43% DTI limit and replaced it with price-based thresholds, mandatory from 1 October 2022. The current text of 12 CFR 1026.43 contains no occurrence of \"43 percent\". The replacement is a price test, and every tier of it comes back in `result.qualifiedMortgage`: in 2026, a first-lien General QM of $137,958 or more must keep its APR **under** the average prime offer rate plus 2.25 percentage points, and 5 further tiers cover smaller loans, manufactured homes and subordinate liens — 12 CFR 1026.43(e)(2)(vi).\n\n**The $110,260 printed in the regulation is not the amount in force, and quoting it is the second-order version of the same mistake.** The section prints 2021 base figures and marks them \"(indexed for inflation)\", then says in as many words to read the official commentary for the current dollar amounts. The Bureau re-indexes them to the CPI-U reported the preceding June and republishes them each December, effective 1 January: the 2026 amounts in `qualifiedMortgage.tiers` are the ones from 90 FR 57890. `meta.dataVintage` carries the date the next edition is due.\n\nDTI did not stop mattering; it stopped being a bright line. A creditor must still consider and verify the borrower's debt-to-income ratio **or** residual income under 12 CFR 1026.43(c)(2)(vii) and (c)(7), with no numeric ceiling attached — which is why `residualIncome` is returned beside the ratios rather than as a nicety.\n\nWhere 43% does still bind: FHA's qualifying ratios for manually underwritten loans are 31%/43%, and above either the lender must justify in writing why the loan is an acceptable risk. 37%/47% is reachable with a documented compensating factor and a credit score of 580 or better (HUD Handbook 4000.1 II.A.5.d).\n\nThe 28/36 pair is a lending convention with no regulation behind it, and is labelled `convention` rather than `fha` in the response for exactly that reason.\n\nA benchmark is cleared only when **both** ratios are at or under it, and the boundary is inclusive: a back-end ratio of exactly 43.0% clears FHA's, since it is exceeding the ratio that triggers the write-up.",
        "externalDocs": {
          "description": "The debt-to-income ratio calculator on farbetteroff.com",
          "url": "https://farbetteroff.com/calculators/debt-to-income-calculator"
        },
        "responses": {
          "200": {
            "$ref": "#/components/responses/debt-to-income-result"
          },
          "400": {
            "description": "A parameter is missing, mistyped, out of range or unknown — all of them are listed at once. A name that is one or two edits from a real parameter is answered with the one it probably meant; an ambiguous one is answered with the accepted list instead of a guess.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such endpoint. The response lists every valid slug.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "More than 60 requests in 60 seconds from one IP.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "A bug on our side. The inputs are never at fault for this one.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "operationId": "get-debt-to-income",
        "parameters": [
          {
            "name": "monthlyIncome",
            "in": "query",
            "required": true,
            "description": "Gross monthly income, before tax and deductions. Monthly, not annual — /api/v1/pay converts a salary if you have the yearly figure. Accepts number, US dollars, between 1 and 10000000.",
            "schema": {
              "type": "number",
              "minimum": 1,
              "maximum": 10000000,
              "x-unit": "usd",
              "examples": [
                6000
              ]
            }
          },
          {
            "name": "housing",
            "in": "query",
            "required": true,
            "description": "Rent, or the mortgage payment including property tax, insurance, HOA dues and mortgage insurance. This is the front-end ratio on its own. Accepts number, US dollars, between 0 and 1000000.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 1000000,
              "x-unit": "usd",
              "examples": [
                1600
              ]
            }
          },
          {
            "name": "autoLoans",
            "in": "query",
            "required": false,
            "description": "Car and other vehicle payments, per month. Accepts number, US dollars, between 0 and 1000000.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 1000000,
              "default": 0,
              "x-unit": "usd",
              "examples": [
                400
              ]
            }
          },
          {
            "name": "studentLoans",
            "in": "query",
            "required": false,
            "description": "Student loan payments, per month. Accepts number, US dollars, between 0 and 1000000.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 1000000,
              "default": 0,
              "x-unit": "usd",
              "examples": [
                250
              ]
            }
          },
          {
            "name": "creditCardMinimums",
            "in": "query",
            "required": false,
            "description": "Minimum payments due, not balances — the ratio is built from payments. Accepts number, US dollars, between 0 and 1000000.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 1000000,
              "default": 0,
              "x-unit": "usd",
              "examples": [
                150
              ]
            }
          },
          {
            "name": "alimonyChildSupport",
            "in": "query",
            "required": false,
            "description": "Alimony and child support. Counted as debt by 12 CFR 1026.43(c)(7)(i)(A), and the item most often left out. Accepts number, US dollars, between 0 and 1000000.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 1000000,
              "default": 0,
              "x-unit": "usd"
            }
          },
          {
            "name": "otherDebt",
            "in": "query",
            "required": false,
            "description": "Personal loans and any other recurring debt obligation. Accepts number, US dollars, between 0 and 1000000.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 1000000,
              "default": 0,
              "x-unit": "usd"
            }
          }
        ]
      },
      "post": {
        "tags": [
          "calculators"
        ],
        "summary": "Debt-to-income ratio",
        "description": "Front-end and back-end DTI, residual income, and which of the underwriting benchmarks that actually govern something the result clears.\n\nGross income, never take-home. DTI is computed on income before tax because that is what a lender uses; comparing debts to net pay overstates the ratio.\n\nWhat counts as debt is defined, not guessed: 12 CFR 1026.43(c)(7)(i)(A) sums the mortgage payment, simultaneous loans, mortgage-related obligations (property tax, insurance, HOA) and current debt obligations including alimony and child support. Everyday living costs — groceries, utilities, phone — are not in that list and are not in this ratio.\n\n**43% is no longer the CFPB's qualified-mortgage limit, and citing it as one is out of date.** The CFPB's General QM Final Rule removed the General QM definition's 43% DTI limit and replaced it with price-based thresholds, mandatory from 1 October 2022. The current text of 12 CFR 1026.43 contains no occurrence of \"43 percent\". The replacement is a price test, and every tier of it comes back in `result.qualifiedMortgage`: in 2026, a first-lien General QM of $137,958 or more must keep its APR **under** the average prime offer rate plus 2.25 percentage points, and 5 further tiers cover smaller loans, manufactured homes and subordinate liens — 12 CFR 1026.43(e)(2)(vi).\n\n**The $110,260 printed in the regulation is not the amount in force, and quoting it is the second-order version of the same mistake.** The section prints 2021 base figures and marks them \"(indexed for inflation)\", then says in as many words to read the official commentary for the current dollar amounts. The Bureau re-indexes them to the CPI-U reported the preceding June and republishes them each December, effective 1 January: the 2026 amounts in `qualifiedMortgage.tiers` are the ones from 90 FR 57890. `meta.dataVintage` carries the date the next edition is due.\n\nDTI did not stop mattering; it stopped being a bright line. A creditor must still consider and verify the borrower's debt-to-income ratio **or** residual income under 12 CFR 1026.43(c)(2)(vii) and (c)(7), with no numeric ceiling attached — which is why `residualIncome` is returned beside the ratios rather than as a nicety.\n\nWhere 43% does still bind: FHA's qualifying ratios for manually underwritten loans are 31%/43%, and above either the lender must justify in writing why the loan is an acceptable risk. 37%/47% is reachable with a documented compensating factor and a credit score of 580 or better (HUD Handbook 4000.1 II.A.5.d).\n\nThe 28/36 pair is a lending convention with no regulation behind it, and is labelled `convention` rather than `fha` in the response for exactly that reason.\n\nA benchmark is cleared only when **both** ratios are at or under it, and the boundary is inclusive: a back-end ratio of exactly 43.0% clears FHA's, since it is exceeding the ratio that triggers the write-up.\n\nIdentical to the GET, with the parameters in a JSON object body.",
        "externalDocs": {
          "description": "The debt-to-income ratio calculator on farbetteroff.com",
          "url": "https://farbetteroff.com/calculators/debt-to-income-calculator"
        },
        "responses": {
          "200": {
            "$ref": "#/components/responses/debt-to-income-result"
          },
          "400": {
            "description": "A parameter is missing, mistyped, out of range or unknown — all of them are listed at once. A name that is one or two edits from a real parameter is answered with the one it probably meant; an ambiguous one is answered with the accepted list instead of a guess.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such endpoint. The response lists every valid slug.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "More than 60 requests in 60 seconds from one IP.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "A bug on our side. The inputs are never at fault for this one.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "operationId": "post-debt-to-income",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "monthlyIncome",
                  "housing"
                ],
                "properties": {
                  "monthlyIncome": {
                    "type": "number",
                    "minimum": 1,
                    "maximum": 10000000,
                    "x-unit": "usd",
                    "examples": [
                      6000
                    ],
                    "description": "Gross monthly income, before tax and deductions. Monthly, not annual — /api/v1/pay converts a salary if you have the yearly figure. Accepts number, US dollars, between 1 and 10000000."
                  },
                  "housing": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 1000000,
                    "x-unit": "usd",
                    "examples": [
                      1600
                    ],
                    "description": "Rent, or the mortgage payment including property tax, insurance, HOA dues and mortgage insurance. This is the front-end ratio on its own. Accepts number, US dollars, between 0 and 1000000."
                  },
                  "autoLoans": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 1000000,
                    "default": 0,
                    "x-unit": "usd",
                    "examples": [
                      400
                    ],
                    "description": "Car and other vehicle payments, per month. Accepts number, US dollars, between 0 and 1000000."
                  },
                  "studentLoans": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 1000000,
                    "default": 0,
                    "x-unit": "usd",
                    "examples": [
                      250
                    ],
                    "description": "Student loan payments, per month. Accepts number, US dollars, between 0 and 1000000."
                  },
                  "creditCardMinimums": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 1000000,
                    "default": 0,
                    "x-unit": "usd",
                    "examples": [
                      150
                    ],
                    "description": "Minimum payments due, not balances — the ratio is built from payments. Accepts number, US dollars, between 0 and 1000000."
                  },
                  "alimonyChildSupport": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 1000000,
                    "default": 0,
                    "x-unit": "usd",
                    "description": "Alimony and child support. Counted as debt by 12 CFR 1026.43(c)(7)(i)(A), and the item most often left out. Accepts number, US dollars, between 0 and 1000000."
                  },
                  "otherDebt": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 1000000,
                    "default": 0,
                    "x-unit": "usd",
                    "description": "Personal loans and any other recurring debt obligation. Accepts number, US dollars, between 0 and 1000000."
                  }
                }
              }
            }
          }
        }
      }
    },
    "/credit-card-payoff": {
      "get": {
        "tags": [
          "calculators"
        ],
        "summary": "Credit card payoff",
        "description": "How long a card balance takes to clear at a fixed monthly payment, and what it costs — including the minimum-payment trap.\n\nThe minimum-payment formula (1% of balance plus that month's interest, with a $25 floor) is the common US issuer convention, not a statutory rule. Your card's terms govern.\n\nAssumes no new charges and no fees.",
        "externalDocs": {
          "description": "The credit card payoff calculator on farbetteroff.com",
          "url": "https://farbetteroff.com/calculators/credit-card-payoff-calculator"
        },
        "responses": {
          "200": {
            "$ref": "#/components/responses/credit-card-payoff-result"
          },
          "400": {
            "description": "A parameter is missing, mistyped, out of range or unknown — all of them are listed at once. A name that is one or two edits from a real parameter is answered with the one it probably meant; an ambiguous one is answered with the accepted list instead of a guess.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such endpoint. The response lists every valid slug.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "More than 60 requests in 60 seconds from one IP.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "A bug on our side. The inputs are never at fault for this one.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "operationId": "get-credit-card-payoff",
        "parameters": [
          {
            "name": "balance",
            "in": "query",
            "required": true,
            "description": "Current balance. Accepts number, US dollars, between 0 and 1000000000.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 1000000000,
              "x-unit": "usd",
              "examples": [
                5000
              ]
            }
          },
          {
            "name": "apr",
            "in": "query",
            "required": true,
            "description": "Annual percentage rate. Accepts number, a percentage, so 6.5 means 6.5%, between 0 and 100.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 100,
              "x-unit": "percent",
              "examples": [
                22.8
              ]
            }
          },
          {
            "name": "monthlyPayment",
            "in": "query",
            "required": false,
            "description": "Fixed amount paid each month. Ignored when minimumOnly=true. Accepts number, US dollars, between 0 and 1000000000.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 1000000000,
              "default": 0,
              "x-unit": "usd",
              "examples": [
                150
              ]
            }
          },
          {
            "name": "minimumOnly",
            "in": "query",
            "required": false,
            "description": "Pay only the card's minimum each month (1% of the balance plus interest, floor $25) instead of a fixed amount. Accepts boolean.",
            "schema": {
              "type": "boolean",
              "default": false
            }
          }
        ]
      },
      "post": {
        "tags": [
          "calculators"
        ],
        "summary": "Credit card payoff",
        "description": "How long a card balance takes to clear at a fixed monthly payment, and what it costs — including the minimum-payment trap.\n\nThe minimum-payment formula (1% of balance plus that month's interest, with a $25 floor) is the common US issuer convention, not a statutory rule. Your card's terms govern.\n\nAssumes no new charges and no fees.\n\nIdentical to the GET, with the parameters in a JSON object body.",
        "externalDocs": {
          "description": "The credit card payoff calculator on farbetteroff.com",
          "url": "https://farbetteroff.com/calculators/credit-card-payoff-calculator"
        },
        "responses": {
          "200": {
            "$ref": "#/components/responses/credit-card-payoff-result"
          },
          "400": {
            "description": "A parameter is missing, mistyped, out of range or unknown — all of them are listed at once. A name that is one or two edits from a real parameter is answered with the one it probably meant; an ambiguous one is answered with the accepted list instead of a guess.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such endpoint. The response lists every valid slug.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "More than 60 requests in 60 seconds from one IP.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "A bug on our side. The inputs are never at fault for this one.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "operationId": "post-credit-card-payoff",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "balance",
                  "apr"
                ],
                "properties": {
                  "balance": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 1000000000,
                    "x-unit": "usd",
                    "examples": [
                      5000
                    ],
                    "description": "Current balance. Accepts number, US dollars, between 0 and 1000000000."
                  },
                  "apr": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 100,
                    "x-unit": "percent",
                    "examples": [
                      22.8
                    ],
                    "description": "Annual percentage rate. Accepts number, a percentage, so 6.5 means 6.5%, between 0 and 100."
                  },
                  "monthlyPayment": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 1000000000,
                    "default": 0,
                    "x-unit": "usd",
                    "examples": [
                      150
                    ],
                    "description": "Fixed amount paid each month. Ignored when minimumOnly=true. Accepts number, US dollars, between 0 and 1000000000."
                  },
                  "minimumOnly": {
                    "type": "boolean",
                    "default": false,
                    "description": "Pay only the card's minimum each month (1% of the balance plus interest, floor $25) instead of a fixed amount. Accepts boolean."
                  }
                }
              }
            }
          }
        }
      }
    },
    "/debt-snowball": {
      "get": {
        "tags": [
          "calculators"
        ],
        "summary": "Debt snowball vs. avalanche",
        "description": "Both payoff orders for up to six debts, run side by side: the queue, the month each debt disappears, the debt-free date and what each method costs in interest.\n\nBoth methods pay every minimum every month and throw everything left at one debt, then roll a cleared debt's minimum onto the next one. The monthly payment is identical; only the order differs. That is why the two plans can be compared at all.\n\nThe avalanche (highest rate first) is never slower or more expensive — it kills the costliest interest first. The snowball (smallest balance first) clears a whole debt sooner, which is what `firstDebtGoneMonth` is for. The CFPB describes both and crowns neither, because the plan you actually finish is the one that works: https://www.consumerfinance.gov/about-us/blog/how-reduce-your-debt/\n\nDebts are slots, not a list: `balance1` … `balance6`, each with its own `apr` and `minimum`. A slot with a zero balance is ignored, so you can send a fixed set of six and leave the unused ones empty. Debts are returned named for the slot they came in on.\n\nInterest is charged monthly on the remaining balance at apr/12, which is how a credit-card statement behaves. Minimums are held constant rather than shrinking with the balance, which is how most people actually pay.\n\nAssumes the rates and minimums stay put, no new debt is added, and the whole `monthlyBudget` keeps being paid even as debts disappear. Stop rolling the freed-up minimums forward and the real payoff stretches out by months.\n\nWhen the payment does not outrun the interest the balances grow forever: `months` is null and `neverPaysOff` is true rather than a 600-month answer being invented. Anything past 50 years is reported the same way.\n\nTies are broken on the other measure, so the queue never depends on the order the debts were sent in: two debts at the same rate are ordered smallest balance first, two at the same balance highest rate first.",
        "externalDocs": {
          "description": "The debt snowball vs. avalanche calculator on farbetteroff.com",
          "url": "https://farbetteroff.com/calculators/debt-snowball-calculator"
        },
        "responses": {
          "200": {
            "$ref": "#/components/responses/debt-snowball-result"
          },
          "400": {
            "description": "A parameter is missing, mistyped, out of range or unknown — all of them are listed at once. A name that is one or two edits from a real parameter is answered with the one it probably meant; an ambiguous one is answered with the accepted list instead of a guess.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such endpoint. The response lists every valid slug.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "More than 60 requests in 60 seconds from one IP.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "A bug on our side. The inputs are never at fault for this one.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "operationId": "get-debt-snowball",
        "parameters": [
          {
            "name": "balance1",
            "in": "query",
            "required": true,
            "description": "Balance owed on the first debt. Send them in any order — the payoff queue is worked out for you. Accepts number, US dollars, between 0 and 10000000.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 10000000,
              "x-unit": "usd",
              "examples": [
                6200
              ]
            }
          },
          {
            "name": "apr1",
            "in": "query",
            "required": false,
            "description": "Annual percentage rate on debt 1, e.g. 24.99. This is what the avalanche orders by. Accepts number, a percentage, so 6.5 means 6.5%, between 0 and 100.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 100,
              "default": 0,
              "x-unit": "percent",
              "examples": [
                24.99
              ]
            }
          },
          {
            "name": "minimum1",
            "in": "query",
            "required": false,
            "description": "Minimum payment due on debt 1 each month — the payment, not the balance. Accepts number, US dollars, between 0 and 1000000.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 1000000,
              "default": 0,
              "x-unit": "usd",
              "examples": [
                155
              ]
            }
          },
          {
            "name": "balance2",
            "in": "query",
            "required": true,
            "description": "Balance owed on debt 2. Accepts number, US dollars, between 0 and 10000000.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 10000000,
              "x-unit": "usd",
              "examples": [
                2100
              ]
            }
          },
          {
            "name": "apr2",
            "in": "query",
            "required": false,
            "description": "Annual percentage rate on debt 2, e.g. 24.99. This is what the avalanche orders by. Accepts number, a percentage, so 6.5 means 6.5%, between 0 and 100.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 100,
              "default": 0,
              "x-unit": "percent",
              "examples": [
                6.5
              ]
            }
          },
          {
            "name": "minimum2",
            "in": "query",
            "required": false,
            "description": "Minimum payment due on debt 2 each month — the payment, not the balance. Accepts number, US dollars, between 0 and 1000000.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 1000000,
              "default": 0,
              "x-unit": "usd",
              "examples": [
                95
              ]
            }
          },
          {
            "name": "balance3",
            "in": "query",
            "required": false,
            "description": "Balance owed on debt 3. Leave it out or send 0 to skip the slot. Accepts number, US dollars, between 0 and 10000000.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 10000000,
              "default": 0,
              "x-unit": "usd",
              "examples": [
                11500
              ]
            }
          },
          {
            "name": "apr3",
            "in": "query",
            "required": false,
            "description": "Annual percentage rate on debt 3, e.g. 24.99. This is what the avalanche orders by. Accepts number, a percentage, so 6.5 means 6.5%, between 0 and 100.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 100,
              "default": 0,
              "x-unit": "percent",
              "examples": [
                18.9
              ]
            }
          },
          {
            "name": "minimum3",
            "in": "query",
            "required": false,
            "description": "Minimum payment due on debt 3 each month — the payment, not the balance. Accepts number, US dollars, between 0 and 1000000.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 1000000,
              "default": 0,
              "x-unit": "usd",
              "examples": [
                260
              ]
            }
          },
          {
            "name": "balance4",
            "in": "query",
            "required": false,
            "description": "Balance owed on debt 4. Leave it out or send 0 to skip the slot. Accepts number, US dollars, between 0 and 10000000.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 10000000,
              "default": 0,
              "x-unit": "usd"
            }
          },
          {
            "name": "apr4",
            "in": "query",
            "required": false,
            "description": "Annual percentage rate on debt 4, e.g. 24.99. This is what the avalanche orders by. Accepts number, a percentage, so 6.5 means 6.5%, between 0 and 100.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 100,
              "default": 0,
              "x-unit": "percent"
            }
          },
          {
            "name": "minimum4",
            "in": "query",
            "required": false,
            "description": "Minimum payment due on debt 4 each month — the payment, not the balance. Accepts number, US dollars, between 0 and 1000000.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 1000000,
              "default": 0,
              "x-unit": "usd"
            }
          },
          {
            "name": "balance5",
            "in": "query",
            "required": false,
            "description": "Balance owed on debt 5. Leave it out or send 0 to skip the slot. Accepts number, US dollars, between 0 and 10000000.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 10000000,
              "default": 0,
              "x-unit": "usd"
            }
          },
          {
            "name": "apr5",
            "in": "query",
            "required": false,
            "description": "Annual percentage rate on debt 5, e.g. 24.99. This is what the avalanche orders by. Accepts number, a percentage, so 6.5 means 6.5%, between 0 and 100.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 100,
              "default": 0,
              "x-unit": "percent"
            }
          },
          {
            "name": "minimum5",
            "in": "query",
            "required": false,
            "description": "Minimum payment due on debt 5 each month — the payment, not the balance. Accepts number, US dollars, between 0 and 1000000.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 1000000,
              "default": 0,
              "x-unit": "usd"
            }
          },
          {
            "name": "balance6",
            "in": "query",
            "required": false,
            "description": "Balance owed on debt 6. Leave it out or send 0 to skip the slot. Accepts number, US dollars, between 0 and 10000000.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 10000000,
              "default": 0,
              "x-unit": "usd"
            }
          },
          {
            "name": "apr6",
            "in": "query",
            "required": false,
            "description": "Annual percentage rate on debt 6, e.g. 24.99. This is what the avalanche orders by. Accepts number, a percentage, so 6.5 means 6.5%, between 0 and 100.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 100,
              "default": 0,
              "x-unit": "percent"
            }
          },
          {
            "name": "minimum6",
            "in": "query",
            "required": false,
            "description": "Minimum payment due on debt 6 each month — the payment, not the balance. Accepts number, US dollars, between 0 and 1000000.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 1000000,
              "default": 0,
              "x-unit": "usd"
            }
          },
          {
            "name": "extra",
            "in": "query",
            "required": false,
            "description": "Everything you can pay above the minimums, per month. This single number decides how fast you are free; the method only decides the order. Accepts number, US dollars, between 0 and 1000000.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 1000000,
              "default": 0,
              "x-unit": "usd",
              "examples": [
                200
              ]
            }
          }
        ]
      },
      "post": {
        "tags": [
          "calculators"
        ],
        "summary": "Debt snowball vs. avalanche",
        "description": "Both payoff orders for up to six debts, run side by side: the queue, the month each debt disappears, the debt-free date and what each method costs in interest.\n\nBoth methods pay every minimum every month and throw everything left at one debt, then roll a cleared debt's minimum onto the next one. The monthly payment is identical; only the order differs. That is why the two plans can be compared at all.\n\nThe avalanche (highest rate first) is never slower or more expensive — it kills the costliest interest first. The snowball (smallest balance first) clears a whole debt sooner, which is what `firstDebtGoneMonth` is for. The CFPB describes both and crowns neither, because the plan you actually finish is the one that works: https://www.consumerfinance.gov/about-us/blog/how-reduce-your-debt/\n\nDebts are slots, not a list: `balance1` … `balance6`, each with its own `apr` and `minimum`. A slot with a zero balance is ignored, so you can send a fixed set of six and leave the unused ones empty. Debts are returned named for the slot they came in on.\n\nInterest is charged monthly on the remaining balance at apr/12, which is how a credit-card statement behaves. Minimums are held constant rather than shrinking with the balance, which is how most people actually pay.\n\nAssumes the rates and minimums stay put, no new debt is added, and the whole `monthlyBudget` keeps being paid even as debts disappear. Stop rolling the freed-up minimums forward and the real payoff stretches out by months.\n\nWhen the payment does not outrun the interest the balances grow forever: `months` is null and `neverPaysOff` is true rather than a 600-month answer being invented. Anything past 50 years is reported the same way.\n\nTies are broken on the other measure, so the queue never depends on the order the debts were sent in: two debts at the same rate are ordered smallest balance first, two at the same balance highest rate first.\n\nIdentical to the GET, with the parameters in a JSON object body.",
        "externalDocs": {
          "description": "The debt snowball vs. avalanche calculator on farbetteroff.com",
          "url": "https://farbetteroff.com/calculators/debt-snowball-calculator"
        },
        "responses": {
          "200": {
            "$ref": "#/components/responses/debt-snowball-result"
          },
          "400": {
            "description": "A parameter is missing, mistyped, out of range or unknown — all of them are listed at once. A name that is one or two edits from a real parameter is answered with the one it probably meant; an ambiguous one is answered with the accepted list instead of a guess.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such endpoint. The response lists every valid slug.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "More than 60 requests in 60 seconds from one IP.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "A bug on our side. The inputs are never at fault for this one.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "operationId": "post-debt-snowball",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "balance1",
                  "balance2"
                ],
                "properties": {
                  "balance1": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 10000000,
                    "x-unit": "usd",
                    "examples": [
                      6200
                    ],
                    "description": "Balance owed on the first debt. Send them in any order — the payoff queue is worked out for you. Accepts number, US dollars, between 0 and 10000000."
                  },
                  "apr1": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 100,
                    "default": 0,
                    "x-unit": "percent",
                    "examples": [
                      24.99
                    ],
                    "description": "Annual percentage rate on debt 1, e.g. 24.99. This is what the avalanche orders by. Accepts number, a percentage, so 6.5 means 6.5%, between 0 and 100."
                  },
                  "minimum1": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 1000000,
                    "default": 0,
                    "x-unit": "usd",
                    "examples": [
                      155
                    ],
                    "description": "Minimum payment due on debt 1 each month — the payment, not the balance. Accepts number, US dollars, between 0 and 1000000."
                  },
                  "balance2": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 10000000,
                    "x-unit": "usd",
                    "examples": [
                      2100
                    ],
                    "description": "Balance owed on debt 2. Accepts number, US dollars, between 0 and 10000000."
                  },
                  "apr2": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 100,
                    "default": 0,
                    "x-unit": "percent",
                    "examples": [
                      6.5
                    ],
                    "description": "Annual percentage rate on debt 2, e.g. 24.99. This is what the avalanche orders by. Accepts number, a percentage, so 6.5 means 6.5%, between 0 and 100."
                  },
                  "minimum2": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 1000000,
                    "default": 0,
                    "x-unit": "usd",
                    "examples": [
                      95
                    ],
                    "description": "Minimum payment due on debt 2 each month — the payment, not the balance. Accepts number, US dollars, between 0 and 1000000."
                  },
                  "balance3": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 10000000,
                    "default": 0,
                    "x-unit": "usd",
                    "examples": [
                      11500
                    ],
                    "description": "Balance owed on debt 3. Leave it out or send 0 to skip the slot. Accepts number, US dollars, between 0 and 10000000."
                  },
                  "apr3": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 100,
                    "default": 0,
                    "x-unit": "percent",
                    "examples": [
                      18.9
                    ],
                    "description": "Annual percentage rate on debt 3, e.g. 24.99. This is what the avalanche orders by. Accepts number, a percentage, so 6.5 means 6.5%, between 0 and 100."
                  },
                  "minimum3": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 1000000,
                    "default": 0,
                    "x-unit": "usd",
                    "examples": [
                      260
                    ],
                    "description": "Minimum payment due on debt 3 each month — the payment, not the balance. Accepts number, US dollars, between 0 and 1000000."
                  },
                  "balance4": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 10000000,
                    "default": 0,
                    "x-unit": "usd",
                    "description": "Balance owed on debt 4. Leave it out or send 0 to skip the slot. Accepts number, US dollars, between 0 and 10000000."
                  },
                  "apr4": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 100,
                    "default": 0,
                    "x-unit": "percent",
                    "description": "Annual percentage rate on debt 4, e.g. 24.99. This is what the avalanche orders by. Accepts number, a percentage, so 6.5 means 6.5%, between 0 and 100."
                  },
                  "minimum4": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 1000000,
                    "default": 0,
                    "x-unit": "usd",
                    "description": "Minimum payment due on debt 4 each month — the payment, not the balance. Accepts number, US dollars, between 0 and 1000000."
                  },
                  "balance5": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 10000000,
                    "default": 0,
                    "x-unit": "usd",
                    "description": "Balance owed on debt 5. Leave it out or send 0 to skip the slot. Accepts number, US dollars, between 0 and 10000000."
                  },
                  "apr5": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 100,
                    "default": 0,
                    "x-unit": "percent",
                    "description": "Annual percentage rate on debt 5, e.g. 24.99. This is what the avalanche orders by. Accepts number, a percentage, so 6.5 means 6.5%, between 0 and 100."
                  },
                  "minimum5": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 1000000,
                    "default": 0,
                    "x-unit": "usd",
                    "description": "Minimum payment due on debt 5 each month — the payment, not the balance. Accepts number, US dollars, between 0 and 1000000."
                  },
                  "balance6": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 10000000,
                    "default": 0,
                    "x-unit": "usd",
                    "description": "Balance owed on debt 6. Leave it out or send 0 to skip the slot. Accepts number, US dollars, between 0 and 10000000."
                  },
                  "apr6": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 100,
                    "default": 0,
                    "x-unit": "percent",
                    "description": "Annual percentage rate on debt 6, e.g. 24.99. This is what the avalanche orders by. Accepts number, a percentage, so 6.5 means 6.5%, between 0 and 100."
                  },
                  "minimum6": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 1000000,
                    "default": 0,
                    "x-unit": "usd",
                    "description": "Minimum payment due on debt 6 each month — the payment, not the balance. Accepts number, US dollars, between 0 and 1000000."
                  },
                  "extra": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 1000000,
                    "default": 0,
                    "x-unit": "usd",
                    "examples": [
                      200
                    ],
                    "description": "Everything you can pay above the minimums, per month. This single number decides how fast you are free; the method only decides the order. Accepts number, US dollars, between 0 and 1000000."
                  }
                }
              }
            }
          }
        }
      }
    },
    "/compound-interest": {
      "get": {
        "tags": [
          "calculators"
        ],
        "summary": "Compound interest",
        "description": "What a balance grows to with regular contributions, split into what you put in and what compounding added.",
        "externalDocs": {
          "description": "The compound interest calculator on farbetteroff.com",
          "url": "https://farbetteroff.com/calculators/compound-interest-calculator"
        },
        "responses": {
          "200": {
            "$ref": "#/components/responses/compound-interest-result"
          },
          "400": {
            "description": "A parameter is missing, mistyped, out of range or unknown — all of them are listed at once. A name that is one or two edits from a real parameter is answered with the one it probably meant; an ambiguous one is answered with the accepted list instead of a guess.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such endpoint. The response lists every valid slug.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "More than 60 requests in 60 seconds from one IP.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "A bug on our side. The inputs are never at fault for this one.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "operationId": "get-compound-interest",
        "parameters": [
          {
            "name": "principal",
            "in": "query",
            "required": true,
            "description": "Starting balance. Accepts number, US dollars, between 0 and 1000000000000.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 1000000000000,
              "x-unit": "usd",
              "examples": [
                10000
              ]
            }
          },
          {
            "name": "contribution",
            "in": "query",
            "required": false,
            "description": "Added at the end of every compounding period. Accepts number, US dollars, between 0 and 1000000000.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 1000000000,
              "default": 0,
              "x-unit": "usd",
              "examples": [
                500
              ]
            }
          },
          {
            "name": "rate",
            "in": "query",
            "required": true,
            "description": "Nominal annual return. Accepts number, a percentage, so 6.5 means 6.5%, between 0 and 100.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 100,
              "x-unit": "percent",
              "examples": [
                7
              ]
            }
          },
          {
            "name": "years",
            "in": "query",
            "required": true,
            "description": "How long to run it. Accepts integer, years, between 0 and 100.",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "maximum": 100,
              "x-unit": "years",
              "examples": [
                25
              ]
            }
          },
          {
            "name": "periodsPerYear",
            "in": "query",
            "required": false,
            "description": "Compounding periods per year. 12 is monthly, 1 is annual. Accepts integer, a count, between 1 and 365.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 365,
              "default": 12,
              "x-unit": "count"
            }
          },
          {
            "name": "series",
            "in": "query",
            "required": false,
            "description": "Include the year-by-year balance series. Accepts boolean.",
            "schema": {
              "type": "boolean",
              "default": false
            }
          }
        ]
      },
      "post": {
        "tags": [
          "calculators"
        ],
        "summary": "Compound interest",
        "description": "What a balance grows to with regular contributions, split into what you put in and what compounding added.\n\nIdentical to the GET, with the parameters in a JSON object body.",
        "externalDocs": {
          "description": "The compound interest calculator on farbetteroff.com",
          "url": "https://farbetteroff.com/calculators/compound-interest-calculator"
        },
        "responses": {
          "200": {
            "$ref": "#/components/responses/compound-interest-result"
          },
          "400": {
            "description": "A parameter is missing, mistyped, out of range or unknown — all of them are listed at once. A name that is one or two edits from a real parameter is answered with the one it probably meant; an ambiguous one is answered with the accepted list instead of a guess.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such endpoint. The response lists every valid slug.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "More than 60 requests in 60 seconds from one IP.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "A bug on our side. The inputs are never at fault for this one.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "operationId": "post-compound-interest",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "principal",
                  "rate",
                  "years"
                ],
                "properties": {
                  "principal": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 1000000000000,
                    "x-unit": "usd",
                    "examples": [
                      10000
                    ],
                    "description": "Starting balance. Accepts number, US dollars, between 0 and 1000000000000."
                  },
                  "contribution": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 1000000000,
                    "default": 0,
                    "x-unit": "usd",
                    "examples": [
                      500
                    ],
                    "description": "Added at the end of every compounding period. Accepts number, US dollars, between 0 and 1000000000."
                  },
                  "rate": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 100,
                    "x-unit": "percent",
                    "examples": [
                      7
                    ],
                    "description": "Nominal annual return. Accepts number, a percentage, so 6.5 means 6.5%, between 0 and 100."
                  },
                  "years": {
                    "type": "integer",
                    "minimum": 0,
                    "maximum": 100,
                    "x-unit": "years",
                    "examples": [
                      25
                    ],
                    "description": "How long to run it. Accepts integer, years, between 0 and 100."
                  },
                  "periodsPerYear": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 365,
                    "default": 12,
                    "x-unit": "count",
                    "description": "Compounding periods per year. 12 is monthly, 1 is annual. Accepts integer, a count, between 1 and 365."
                  },
                  "series": {
                    "type": "boolean",
                    "default": false,
                    "description": "Include the year-by-year balance series. Accepts boolean."
                  }
                }
              }
            }
          }
        }
      }
    },
    "/savings-goal": {
      "get": {
        "tags": [
          "calculators"
        ],
        "summary": "Savings goal",
        "description": "Both halves of a savings target: how long a monthly deposit takes to get there, and what deposit hits a deadline exactly.",
        "externalDocs": {
          "description": "The savings goal calculator on farbetteroff.com",
          "url": "https://farbetteroff.com/calculators/savings-goal-calculator"
        },
        "responses": {
          "200": {
            "$ref": "#/components/responses/savings-goal-result"
          },
          "400": {
            "description": "A parameter is missing, mistyped, out of range or unknown — all of them are listed at once. A name that is one or two edits from a real parameter is answered with the one it probably meant; an ambiguous one is answered with the accepted list instead of a guess.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such endpoint. The response lists every valid slug.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "More than 60 requests in 60 seconds from one IP.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "A bug on our side. The inputs are never at fault for this one.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "operationId": "get-savings-goal",
        "parameters": [
          {
            "name": "goal",
            "in": "query",
            "required": true,
            "description": "The amount you are aiming at. Accepts number, US dollars, between 0 and 1000000000000.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 1000000000000,
              "x-unit": "usd",
              "examples": [
                30000
              ]
            }
          },
          {
            "name": "current",
            "in": "query",
            "required": false,
            "description": "What you have saved already. Accepts number, US dollars, between 0 and 1000000000000.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 1000000000000,
              "default": 0,
              "x-unit": "usd",
              "examples": [
                5000
              ]
            }
          },
          {
            "name": "monthly",
            "in": "query",
            "required": false,
            "description": "What you put in each month. Drives the time-to-goal answer. Accepts number, US dollars, between 0 and 1000000000.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 1000000000,
              "default": 0,
              "x-unit": "usd",
              "examples": [
                400
              ]
            }
          },
          {
            "name": "rate",
            "in": "query",
            "required": false,
            "description": "Annual return or APY on the savings. Accepts number, a percentage, so 6.5 means 6.5%, between 0 and 100.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 100,
              "default": 0,
              "x-unit": "percent",
              "examples": [
                4
              ]
            }
          },
          {
            "name": "deadlineYears",
            "in": "query",
            "required": false,
            "description": "Optional deadline. When above 0, the response also says what monthly deposit lands on the goal exactly then. Accepts number, years, between 0 and 100.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 100,
              "default": 0,
              "x-unit": "years",
              "examples": [
                5
              ]
            }
          }
        ]
      },
      "post": {
        "tags": [
          "calculators"
        ],
        "summary": "Savings goal",
        "description": "Both halves of a savings target: how long a monthly deposit takes to get there, and what deposit hits a deadline exactly.\n\nIdentical to the GET, with the parameters in a JSON object body.",
        "externalDocs": {
          "description": "The savings goal calculator on farbetteroff.com",
          "url": "https://farbetteroff.com/calculators/savings-goal-calculator"
        },
        "responses": {
          "200": {
            "$ref": "#/components/responses/savings-goal-result"
          },
          "400": {
            "description": "A parameter is missing, mistyped, out of range or unknown — all of them are listed at once. A name that is one or two edits from a real parameter is answered with the one it probably meant; an ambiguous one is answered with the accepted list instead of a guess.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such endpoint. The response lists every valid slug.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "More than 60 requests in 60 seconds from one IP.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "A bug on our side. The inputs are never at fault for this one.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "operationId": "post-savings-goal",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "goal"
                ],
                "properties": {
                  "goal": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 1000000000000,
                    "x-unit": "usd",
                    "examples": [
                      30000
                    ],
                    "description": "The amount you are aiming at. Accepts number, US dollars, between 0 and 1000000000000."
                  },
                  "current": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 1000000000000,
                    "default": 0,
                    "x-unit": "usd",
                    "examples": [
                      5000
                    ],
                    "description": "What you have saved already. Accepts number, US dollars, between 0 and 1000000000000."
                  },
                  "monthly": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 1000000000,
                    "default": 0,
                    "x-unit": "usd",
                    "examples": [
                      400
                    ],
                    "description": "What you put in each month. Drives the time-to-goal answer. Accepts number, US dollars, between 0 and 1000000000."
                  },
                  "rate": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 100,
                    "default": 0,
                    "x-unit": "percent",
                    "examples": [
                      4
                    ],
                    "description": "Annual return or APY on the savings. Accepts number, a percentage, so 6.5 means 6.5%, between 0 and 100."
                  },
                  "deadlineYears": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 100,
                    "default": 0,
                    "x-unit": "years",
                    "examples": [
                      5
                    ],
                    "description": "Optional deadline. When above 0, the response also says what monthly deposit lands on the goal exactly then. Accepts number, years, between 0 and 100."
                  }
                }
              }
            }
          }
        }
      }
    },
    "/emergency-fund": {
      "get": {
        "tags": [
          "calculators"
        ],
        "summary": "Emergency fund",
        "description": "How big a cushion is, how much of it is already there, and how many months of expenses today's savings would actually cover.\n\n**Nobody regulates the size of an emergency fund, and this endpoint does not pretend otherwise.** \"Three to six months\" is a widely-repeated convention with no rule behind it. The CFPB's own guide to building an emergency fund gives no month-count target at all — it says to save what you can, and that \"even a small amount can provide some financial security\". So `ladder` is labelled a set of conventions, and the numbers here that do have a measurement behind them are kept separate in `benchmarks`.\n\n`monthsCovered` is the number to quote, not `fundedPct`. A percentage is a ratio against a target somebody chose, so it halves when a caller changes `months` from 6 to 12 while nothing about the household has changed. Months covered is what the Federal Reserve's Survey of Household Economics and Decisionmaking actually asks about: whether a household \"could cover three months of expenses with a rainy day fund\".\n\nThe $400 figure in `coversSmallShock` is the Fed's small-emergency question — whether an adult could cover \"a hypothetical $400 emergency expense exclusively using cash, savings, or a credit card paid off at the next statement\". 63% of US adults said they could in the 2025 survey, unchanged from the previous several years and down from a high of 68% in 2021. The three-month cushion in `atRainyDayBenchmark` is the other measured one: 55% of US adults reached it, while 30% could not cover three months by any means at all — borrowing and selling included. Both distributions, by income and by age, are set out at /how-much-should-i-have-in-an-emergency-fund with the table citations.\n\nInterest is deliberately ignored. An emergency fund belongs somewhere liquid, and over the months this projection covers APY moves the finish line by days, so `monthsToTarget` is a slight over-estimate — the safe direction to be wrong in. Use /api/v1/savings-goal if you want the same question answered with a return applied.\n\n`monthsToTarget` distinguishes 0 from null: 0 means the target is already met, null means not on this plan. A caller that conflates them prints \"never\" to someone who has finished.\n\n**The spread is the finding, not the row a caller lands on.** The same \"three to six months\" is advised to a household where **21%** manage it and to one where **75%** do, and across age it runs 37% to 71% — both bracketing the 55% headline that gets quoted on its own. A single national figure describes neither end, which is why every band travels with the one that was selected.\n\n**No figure here is interpolated between bands.** The bands are the Federal Reserve's own and are reproduced as published rather than re-cut, so a household earning $50,000 and one earning $99,000 get the same 55% — that is the resolution the survey publishes, and inventing a curve between the bands would be a figure the Board never reported. Edges are half-open on income ($50,000 belongs to the band above, matching the Board's \"$25,000–$49,999\" labels) and inclusive on age.\n\n`savingsCeiling` places `saved` in table 26 — \"the largest emergency expense you could handle right now using only savings\". `pctBelowBand` is summed from the published bands rather than read off a cumulative column the Board does not print, and the sum is checkable: the at-or-above share of the $500–$999 band comes to **70%**, which is the figure the Board states in its own text. One caveat worth passing on: the survey asks about **every** dollar of savings, while `saved` is what a household has earmarked for emergencies, so a real ceiling may sit a band higher.",
        "externalDocs": {
          "description": "The emergency fund calculator on farbetteroff.com",
          "url": "https://farbetteroff.com/calculators/emergency-fund-calculator"
        },
        "responses": {
          "200": {
            "$ref": "#/components/responses/emergency-fund-result"
          },
          "400": {
            "description": "A parameter is missing, mistyped, out of range or unknown — all of them are listed at once. A name that is one or two edits from a real parameter is answered with the one it probably meant; an ambiguous one is answered with the accepted list instead of a guess.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such endpoint. The response lists every valid slug.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "More than 60 requests in 60 seconds from one IP.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "A bug on our side. The inputs are never at fault for this one.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "operationId": "get-emergency-fund",
        "parameters": [
          {
            "name": "monthlyExpenses",
            "in": "query",
            "required": true,
            "description": "Essential monthly outgoings: housing, food, utilities, transport, insurance and minimum debt payments. Essentials, not the whole budget — restaurants, holidays and subscriptions pause in an emergency. Accepts number, US dollars, between 0 and 1000000.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 1000000,
              "x-unit": "usd",
              "examples": [
                3500
              ]
            }
          },
          {
            "name": "months",
            "in": "query",
            "required": false,
            "description": "Months of cushion to size the target against. Defaults to 6, which is a convention rather than a rule — see the notes. `ladder` answers for 1, 3, 6 and 12 regardless of what you send. Accepts number, whole months, between 1 and 24.",
            "schema": {
              "type": "number",
              "minimum": 1,
              "maximum": 24,
              "default": 6,
              "x-unit": "months",
              "examples": [
                6
              ]
            }
          },
          {
            "name": "saved",
            "in": "query",
            "required": false,
            "description": "What is set aside for emergencies today. Accepts number, US dollars, between 0 and 1000000000.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 1000000000,
              "default": 0,
              "x-unit": "usd",
              "examples": [
                4000
              ]
            }
          },
          {
            "name": "monthlySaving",
            "in": "query",
            "required": false,
            "description": "What can be added each month. Drives `monthsToTarget`; leave it out and that answer is null. Accepts number, US dollars, between 0 and 1000000.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 1000000,
              "default": 0,
              "x-unit": "usd",
              "examples": [
                400
              ]
            }
          },
          {
            "name": "annualIncome",
            "in": "query",
            "required": false,
            "description": "Annual pre-tax family income. Drives `peers.byIncome` only — it changes no arithmetic. Omit it and that cut comes back null. Accepts number, US dollars, between 0 and 100000000.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 100000000,
              "default": 0,
              "x-unit": "usd",
              "examples": [
                60000
              ]
            }
          },
          {
            "name": "age",
            "in": "query",
            "required": false,
            "description": "Age of the adult. Drives `peers.byAge` only — it changes no arithmetic. The survey covers adults, so an age under 18 falls outside every published band and returns null. Accepts number, years, between 0 and 120.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 120,
              "default": 0,
              "x-unit": "years",
              "examples": [
                35
              ]
            }
          }
        ]
      },
      "post": {
        "tags": [
          "calculators"
        ],
        "summary": "Emergency fund",
        "description": "How big a cushion is, how much of it is already there, and how many months of expenses today's savings would actually cover.\n\n**Nobody regulates the size of an emergency fund, and this endpoint does not pretend otherwise.** \"Three to six months\" is a widely-repeated convention with no rule behind it. The CFPB's own guide to building an emergency fund gives no month-count target at all — it says to save what you can, and that \"even a small amount can provide some financial security\". So `ladder` is labelled a set of conventions, and the numbers here that do have a measurement behind them are kept separate in `benchmarks`.\n\n`monthsCovered` is the number to quote, not `fundedPct`. A percentage is a ratio against a target somebody chose, so it halves when a caller changes `months` from 6 to 12 while nothing about the household has changed. Months covered is what the Federal Reserve's Survey of Household Economics and Decisionmaking actually asks about: whether a household \"could cover three months of expenses with a rainy day fund\".\n\nThe $400 figure in `coversSmallShock` is the Fed's small-emergency question — whether an adult could cover \"a hypothetical $400 emergency expense exclusively using cash, savings, or a credit card paid off at the next statement\". 63% of US adults said they could in the 2025 survey, unchanged from the previous several years and down from a high of 68% in 2021. The three-month cushion in `atRainyDayBenchmark` is the other measured one: 55% of US adults reached it, while 30% could not cover three months by any means at all — borrowing and selling included. Both distributions, by income and by age, are set out at /how-much-should-i-have-in-an-emergency-fund with the table citations.\n\nInterest is deliberately ignored. An emergency fund belongs somewhere liquid, and over the months this projection covers APY moves the finish line by days, so `monthsToTarget` is a slight over-estimate — the safe direction to be wrong in. Use /api/v1/savings-goal if you want the same question answered with a return applied.\n\n`monthsToTarget` distinguishes 0 from null: 0 means the target is already met, null means not on this plan. A caller that conflates them prints \"never\" to someone who has finished.\n\n**The spread is the finding, not the row a caller lands on.** The same \"three to six months\" is advised to a household where **21%** manage it and to one where **75%** do, and across age it runs 37% to 71% — both bracketing the 55% headline that gets quoted on its own. A single national figure describes neither end, which is why every band travels with the one that was selected.\n\n**No figure here is interpolated between bands.** The bands are the Federal Reserve's own and are reproduced as published rather than re-cut, so a household earning $50,000 and one earning $99,000 get the same 55% — that is the resolution the survey publishes, and inventing a curve between the bands would be a figure the Board never reported. Edges are half-open on income ($50,000 belongs to the band above, matching the Board's \"$25,000–$49,999\" labels) and inclusive on age.\n\n`savingsCeiling` places `saved` in table 26 — \"the largest emergency expense you could handle right now using only savings\". `pctBelowBand` is summed from the published bands rather than read off a cumulative column the Board does not print, and the sum is checkable: the at-or-above share of the $500–$999 band comes to **70%**, which is the figure the Board states in its own text. One caveat worth passing on: the survey asks about **every** dollar of savings, while `saved` is what a household has earmarked for emergencies, so a real ceiling may sit a band higher.\n\nIdentical to the GET, with the parameters in a JSON object body.",
        "externalDocs": {
          "description": "The emergency fund calculator on farbetteroff.com",
          "url": "https://farbetteroff.com/calculators/emergency-fund-calculator"
        },
        "responses": {
          "200": {
            "$ref": "#/components/responses/emergency-fund-result"
          },
          "400": {
            "description": "A parameter is missing, mistyped, out of range or unknown — all of them are listed at once. A name that is one or two edits from a real parameter is answered with the one it probably meant; an ambiguous one is answered with the accepted list instead of a guess.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such endpoint. The response lists every valid slug.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "More than 60 requests in 60 seconds from one IP.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "A bug on our side. The inputs are never at fault for this one.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "operationId": "post-emergency-fund",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "monthlyExpenses"
                ],
                "properties": {
                  "monthlyExpenses": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 1000000,
                    "x-unit": "usd",
                    "examples": [
                      3500
                    ],
                    "description": "Essential monthly outgoings: housing, food, utilities, transport, insurance and minimum debt payments. Essentials, not the whole budget — restaurants, holidays and subscriptions pause in an emergency. Accepts number, US dollars, between 0 and 1000000."
                  },
                  "months": {
                    "type": "number",
                    "minimum": 1,
                    "maximum": 24,
                    "default": 6,
                    "x-unit": "months",
                    "examples": [
                      6
                    ],
                    "description": "Months of cushion to size the target against. Defaults to 6, which is a convention rather than a rule — see the notes. `ladder` answers for 1, 3, 6 and 12 regardless of what you send. Accepts number, whole months, between 1 and 24."
                  },
                  "saved": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 1000000000,
                    "default": 0,
                    "x-unit": "usd",
                    "examples": [
                      4000
                    ],
                    "description": "What is set aside for emergencies today. Accepts number, US dollars, between 0 and 1000000000."
                  },
                  "monthlySaving": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 1000000,
                    "default": 0,
                    "x-unit": "usd",
                    "examples": [
                      400
                    ],
                    "description": "What can be added each month. Drives `monthsToTarget`; leave it out and that answer is null. Accepts number, US dollars, between 0 and 1000000."
                  },
                  "annualIncome": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 100000000,
                    "default": 0,
                    "x-unit": "usd",
                    "examples": [
                      60000
                    ],
                    "description": "Annual pre-tax family income. Drives `peers.byIncome` only — it changes no arithmetic. Omit it and that cut comes back null. Accepts number, US dollars, between 0 and 100000000."
                  },
                  "age": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 120,
                    "default": 0,
                    "x-unit": "years",
                    "examples": [
                      35
                    ],
                    "description": "Age of the adult. Drives `peers.byAge` only — it changes no arithmetic. The survey covers adults, so an age under 18 falls outside every published band and returns null. Accepts number, years, between 0 and 120."
                  }
                }
              }
            }
          }
        }
      }
    },
    "/net-worth": {
      "get": {
        "tags": [
          "calculators"
        ],
        "summary": "Net worth",
        "description": "Everything owned minus everything owed, split into what is liquid and what is not — and where the answer sits against the Federal Reserve's measured distribution for the household's age.\n\n**Quote the median, not the mean.** Net worth is among the most skewed quantities in household finance, so its average describes almost nobody: across all US families in the 2022 SCF the mean is $1,059,470 against a median of $192,700 — the mean is **5.5×** the median, pulled there by the top of the distribution. Every \"average net worth by age\" figure in circulation is a mean. This endpoint returns both, and `meanToMedianRatio` so the difference is visible rather than implied.\n\n**The benchmark figures are 2022 dollars from a 2022 survey, and this endpoint does not pretend they are today's.** The SCF runs every three years and the Board states that the 2022 survey \"is the most recent survey conducted\". Comparing a present-day balance sheet against a 2022-dollar benchmark flatters the present-day one, so `surveyYear` and `dollarYear` travel with every comparison. Nothing here inflates them to a current year — that would invent figures the Federal Reserve never published.\n\nEvery figure was transcribed from the Fed's own published workbook (table 4, \"Family net worth, by selected characteristics of families, 1989–2022 surveys\"), not from a secondary summary: https://www.federalreserve.gov/econres/files/scf2022_tables_public_real_historical.xlsx\n\nThe age bands are the Federal Reserve's own and are not evenly sized (`Less than 35`, then 35–44, 45–54, 55–64, 65–74, `75 or more`). They are reproduced as published rather than re-cut, because re-cutting them needs data the summary table does not carry.\n\nA negative net worth is a normal starting point, not a failure state: a new graduate with student loans or a recent buyer with a fresh mortgage will often show one for years. `negative` is reported as a fact about the balance sheet, and nothing in this response treats it as a verdict.\n\nAssets are counted at what they would sell for today, not at what they cost. That is why a car normally pulls this number down year over year, and why nothing here applies appreciation, depreciation or a growth rate — net worth is a snapshot, and projecting it forward would be a forecast wearing a measurement's clothes.\n\n`retirement` counts toward `netWorth` but not toward `liquidAssets`, and `home` counts toward neither `liquidAssets` nor `liquidNetWorth`. A household can be comfortably positive and still unable to cover a $400 emergency — use /api/v1/emergency-fund for that question.",
        "externalDocs": {
          "description": "The net worth calculator on farbetteroff.com",
          "url": "https://farbetteroff.com/calculators/net-worth-calculator"
        },
        "responses": {
          "200": {
            "$ref": "#/components/responses/net-worth-result"
          },
          "400": {
            "description": "A parameter is missing, mistyped, out of range or unknown — all of them are listed at once. A name that is one or two edits from a real parameter is answered with the one it probably meant; an ambiguous one is answered with the accepted list instead of a guess.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such endpoint. The response lists every valid slug.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "More than 60 requests in 60 seconds from one IP.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "A bug on our side. The inputs are never at fault for this one.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "operationId": "get-net-worth",
        "parameters": [
          {
            "name": "cash",
            "in": "query",
            "required": false,
            "description": "Cash, checking and savings. Accepts number, US dollars, between 0 and 1000000000.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 1000000000,
              "default": 0,
              "x-unit": "usd",
              "examples": [
                12000
              ]
            }
          },
          {
            "name": "investments",
            "in": "query",
            "required": false,
            "description": "Taxable brokerage holdings. Counted as liquid alongside `cash`. Accepts number, US dollars, between 0 and 1000000000.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 1000000000,
              "default": 0,
              "x-unit": "usd",
              "examples": [
                20000
              ]
            }
          },
          {
            "name": "retirement",
            "in": "query",
            "required": false,
            "description": "401(k), IRA and other retirement accounts. Counted as an asset but **not** as liquid — reaching it early generally costs tax and a penalty. Accepts number, US dollars, between 0 and 1000000000.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 1000000000,
              "default": 0,
              "x-unit": "usd",
              "examples": [
                45000
              ]
            }
          },
          {
            "name": "home",
            "in": "query",
            "required": false,
            "description": "Current market value of the home — what it would sell for today, not its purchase price. Accepts number, US dollars, between 0 and 1000000000.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 1000000000,
              "default": 0,
              "x-unit": "usd",
              "examples": [
                320000
              ]
            }
          },
          {
            "name": "vehicles",
            "in": "query",
            "required": false,
            "description": "Resale value of vehicles. See the notes on why this one normally falls year over year. Accepts number, US dollars, between 0 and 1000000000.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 1000000000,
              "default": 0,
              "x-unit": "usd",
              "examples": [
                18000
              ]
            }
          },
          {
            "name": "otherAssets",
            "in": "query",
            "required": false,
            "description": "Business interests, collectibles, cash value of insurance, anything else owned. Accepts number, US dollars, between 0 and 1000000000.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 1000000000,
              "default": 0,
              "x-unit": "usd"
            }
          },
          {
            "name": "mortgage",
            "in": "query",
            "required": false,
            "description": "Outstanding mortgage principal. Netted against `home` to give `homeEquity`. Accepts number, US dollars, between 0 and 1000000000.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 1000000000,
              "default": 0,
              "x-unit": "usd",
              "examples": [
                250000
              ]
            }
          },
          {
            "name": "autoLoans",
            "in": "query",
            "required": false,
            "description": "Auto loan balances. Accepts number, US dollars, between 0 and 1000000000.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 1000000000,
              "default": 0,
              "x-unit": "usd",
              "examples": [
                9000
              ]
            }
          },
          {
            "name": "studentLoans",
            "in": "query",
            "required": false,
            "description": "Student loan balances. Accepts number, US dollars, between 0 and 1000000000.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 1000000000,
              "default": 0,
              "x-unit": "usd",
              "examples": [
                14000
              ]
            }
          },
          {
            "name": "creditCards",
            "in": "query",
            "required": false,
            "description": "Credit card balances carried. Subtracted from liquid assets to give `liquidNetWorth`. Accepts number, US dollars, between 0 and 1000000000.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 1000000000,
              "default": 0,
              "x-unit": "usd",
              "examples": [
                3000
              ]
            }
          },
          {
            "name": "otherDebts",
            "in": "query",
            "required": false,
            "description": "Personal loans, medical debt, anything else owed. Also treated as short-term debt. Accepts number, US dollars, between 0 and 1000000000.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 1000000000,
              "default": 0,
              "x-unit": "usd"
            }
          },
          {
            "name": "age",
            "in": "query",
            "required": false,
            "description": "Age of the household's reference person. Drives `benchmark` only — it changes no arithmetic. Omit it and `benchmark` comes back null. Accepts number, years, between 0 and 120.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 120,
              "default": 0,
              "x-unit": "years",
              "examples": [
                40
              ]
            }
          }
        ]
      },
      "post": {
        "tags": [
          "calculators"
        ],
        "summary": "Net worth",
        "description": "Everything owned minus everything owed, split into what is liquid and what is not — and where the answer sits against the Federal Reserve's measured distribution for the household's age.\n\n**Quote the median, not the mean.** Net worth is among the most skewed quantities in household finance, so its average describes almost nobody: across all US families in the 2022 SCF the mean is $1,059,470 against a median of $192,700 — the mean is **5.5×** the median, pulled there by the top of the distribution. Every \"average net worth by age\" figure in circulation is a mean. This endpoint returns both, and `meanToMedianRatio` so the difference is visible rather than implied.\n\n**The benchmark figures are 2022 dollars from a 2022 survey, and this endpoint does not pretend they are today's.** The SCF runs every three years and the Board states that the 2022 survey \"is the most recent survey conducted\". Comparing a present-day balance sheet against a 2022-dollar benchmark flatters the present-day one, so `surveyYear` and `dollarYear` travel with every comparison. Nothing here inflates them to a current year — that would invent figures the Federal Reserve never published.\n\nEvery figure was transcribed from the Fed's own published workbook (table 4, \"Family net worth, by selected characteristics of families, 1989–2022 surveys\"), not from a secondary summary: https://www.federalreserve.gov/econres/files/scf2022_tables_public_real_historical.xlsx\n\nThe age bands are the Federal Reserve's own and are not evenly sized (`Less than 35`, then 35–44, 45–54, 55–64, 65–74, `75 or more`). They are reproduced as published rather than re-cut, because re-cutting them needs data the summary table does not carry.\n\nA negative net worth is a normal starting point, not a failure state: a new graduate with student loans or a recent buyer with a fresh mortgage will often show one for years. `negative` is reported as a fact about the balance sheet, and nothing in this response treats it as a verdict.\n\nAssets are counted at what they would sell for today, not at what they cost. That is why a car normally pulls this number down year over year, and why nothing here applies appreciation, depreciation or a growth rate — net worth is a snapshot, and projecting it forward would be a forecast wearing a measurement's clothes.\n\n`retirement` counts toward `netWorth` but not toward `liquidAssets`, and `home` counts toward neither `liquidAssets` nor `liquidNetWorth`. A household can be comfortably positive and still unable to cover a $400 emergency — use /api/v1/emergency-fund for that question.\n\nIdentical to the GET, with the parameters in a JSON object body.",
        "externalDocs": {
          "description": "The net worth calculator on farbetteroff.com",
          "url": "https://farbetteroff.com/calculators/net-worth-calculator"
        },
        "responses": {
          "200": {
            "$ref": "#/components/responses/net-worth-result"
          },
          "400": {
            "description": "A parameter is missing, mistyped, out of range or unknown — all of them are listed at once. A name that is one or two edits from a real parameter is answered with the one it probably meant; an ambiguous one is answered with the accepted list instead of a guess.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such endpoint. The response lists every valid slug.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "More than 60 requests in 60 seconds from one IP.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "A bug on our side. The inputs are never at fault for this one.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "operationId": "post-net-worth",
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "properties": {
                  "cash": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 1000000000,
                    "default": 0,
                    "x-unit": "usd",
                    "examples": [
                      12000
                    ],
                    "description": "Cash, checking and savings. Accepts number, US dollars, between 0 and 1000000000."
                  },
                  "investments": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 1000000000,
                    "default": 0,
                    "x-unit": "usd",
                    "examples": [
                      20000
                    ],
                    "description": "Taxable brokerage holdings. Counted as liquid alongside `cash`. Accepts number, US dollars, between 0 and 1000000000."
                  },
                  "retirement": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 1000000000,
                    "default": 0,
                    "x-unit": "usd",
                    "examples": [
                      45000
                    ],
                    "description": "401(k), IRA and other retirement accounts. Counted as an asset but **not** as liquid — reaching it early generally costs tax and a penalty. Accepts number, US dollars, between 0 and 1000000000."
                  },
                  "home": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 1000000000,
                    "default": 0,
                    "x-unit": "usd",
                    "examples": [
                      320000
                    ],
                    "description": "Current market value of the home — what it would sell for today, not its purchase price. Accepts number, US dollars, between 0 and 1000000000."
                  },
                  "vehicles": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 1000000000,
                    "default": 0,
                    "x-unit": "usd",
                    "examples": [
                      18000
                    ],
                    "description": "Resale value of vehicles. See the notes on why this one normally falls year over year. Accepts number, US dollars, between 0 and 1000000000."
                  },
                  "otherAssets": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 1000000000,
                    "default": 0,
                    "x-unit": "usd",
                    "description": "Business interests, collectibles, cash value of insurance, anything else owned. Accepts number, US dollars, between 0 and 1000000000."
                  },
                  "mortgage": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 1000000000,
                    "default": 0,
                    "x-unit": "usd",
                    "examples": [
                      250000
                    ],
                    "description": "Outstanding mortgage principal. Netted against `home` to give `homeEquity`. Accepts number, US dollars, between 0 and 1000000000."
                  },
                  "autoLoans": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 1000000000,
                    "default": 0,
                    "x-unit": "usd",
                    "examples": [
                      9000
                    ],
                    "description": "Auto loan balances. Accepts number, US dollars, between 0 and 1000000000."
                  },
                  "studentLoans": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 1000000000,
                    "default": 0,
                    "x-unit": "usd",
                    "examples": [
                      14000
                    ],
                    "description": "Student loan balances. Accepts number, US dollars, between 0 and 1000000000."
                  },
                  "creditCards": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 1000000000,
                    "default": 0,
                    "x-unit": "usd",
                    "examples": [
                      3000
                    ],
                    "description": "Credit card balances carried. Subtracted from liquid assets to give `liquidNetWorth`. Accepts number, US dollars, between 0 and 1000000000."
                  },
                  "otherDebts": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 1000000000,
                    "default": 0,
                    "x-unit": "usd",
                    "description": "Personal loans, medical debt, anything else owed. Also treated as short-term debt. Accepts number, US dollars, between 0 and 1000000000."
                  },
                  "age": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 120,
                    "default": 0,
                    "x-unit": "years",
                    "examples": [
                      40
                    ],
                    "description": "Age of the household's reference person. Drives `benchmark` only — it changes no arithmetic. Omit it and `benchmark` comes back null. Accepts number, years, between 0 and 120."
                  }
                }
              }
            }
          }
        }
      }
    },
    "/budget": {
      "get": {
        "tags": [
          "calculators"
        ],
        "summary": "50/30/20 budget",
        "description": "Take-home pay split into needs, wants and savings on the 50/30/20 rule — with housing measured against the needs slice it has to fit inside, and against the two benchmarks that are actually published.\n\n**50/30/20 is a rule of thumb from a book, not a standard.** It traces to the 2005 book **All Your Worth: The Ultimate Lifetime Money Plan**, by Elizabeth Warren and Amelia Warren Tyagi. No regulator publishes it, no agency measures against it, and no federal survey reports it as a norm. This endpoint implements it faithfully and returns `ruleOrigin` with every response, so a caller quoting the split can say where it came from instead of implying an authority that does not exist.\n\n**shareOfNeedsBudgetPct is the figure to quote, and nobody else publishes it.** Everyone reports housing as a share of income; the question the rule actually poses is whether housing fits inside the 50% it allows for **all** needs. On $4,500 of take-home the needs slice is $2,250, so $1,800 of rent is **80% of the entire needs budget** and leaves $450 for food, utilities, transport, insurance and minimum debt payments. Above 100 the rule is broken before groceries. That is the arithmetic answer to \"is 50/30/20 realistic?\".\n\n**HUD's 30% is defined on gross income, and this endpoint will not apply it to take-home pay.** HUD defines affordable housing as housing \"on which the occupant is paying no more than 30 percent of gross income for housing costs, including utilities\", above 50 percent being the severe case; its CHAS dataset counts cost-burdened households against exactly that threshold. Take-home pay is a smaller denominator, so the same rent scores higher against it — $1,800 is 40% of $4,500 take-home but 30.0% of $6,000 gross, which is one household and two different verdicts. `housing.hud` therefore stays null until `grossMonthlyIncome` arrives rather than guessing a gross figure. Sources: https://archives.hud.gov/local/nv/goodstories/2006-04-06glos.cfm and https://www.huduser.gov/portal/datasets/cp.html\n\n**Saving 20% is not the norm, and there is a measurement for it.** The Federal Reserve's Survey of Household Economics and Decisionmaking asks how often people have money left over at the end of the month. In 2025, **41 percent** of US adults said always or often — similar to 2024 — and it ranges from 19 percent under $25,000 of family income to 59 percent at $100,000 or more. The rule's savings slice is a target, and `margin` is what the population actually reports. https://www.federalreserve.gov/publications/2026-economic-well-being-of-us-households-in-2025-income-and-expenses.htm\n\nThe Board publishes whole percents, so the five-point distributions in `margin.bands` sum to between 99 and 101 rather than exactly 100. That is rounding in the source. `alwaysOrOftenPct` is the Board's own stated figure rather than a sum of two rounded components — which matters in the lowest band, where the prose says 19 and `often + always` sums to 20.\n\n`annualIncome` is pre-tax family income for the whole household, because that is the basis the Fed's bands use. `income` is monthly take-home for one budget. They are deliberately different questions and neither is derived from the other.\n\nNothing here is advice about what the split **should** be. The rule is returned as the rule, the two benchmarks are returned as measurements, and they are kept in separate objects so nothing borrows the other's authority.",
        "externalDocs": {
          "description": "The 50/30/20 budget calculator on farbetteroff.com",
          "url": "https://farbetteroff.com/calculators/budget-calculator"
        },
        "responses": {
          "200": {
            "$ref": "#/components/responses/budget-result"
          },
          "400": {
            "description": "A parameter is missing, mistyped, out of range or unknown — all of them are listed at once. A name that is one or two edits from a real parameter is answered with the one it probably meant; an ambiguous one is answered with the accepted list instead of a guess.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such endpoint. The response lists every valid slug.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "More than 60 requests in 60 seconds from one IP.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "A bug on our side. The inputs are never at fault for this one.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "operationId": "get-budget",
        "parameters": [
          {
            "name": "income",
            "in": "query",
            "required": true,
            "description": "Monthly **take-home** pay — after taxes and payroll deductions. Accepts number, US dollars, between 0 and 10000000.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 10000000,
              "x-unit": "usd",
              "examples": [
                4500
              ]
            }
          },
          {
            "name": "housing",
            "in": "query",
            "required": false,
            "description": "Monthly housing cost **including utilities**, HUD's basis. Drives the whole `housing` object; omit it and that comes back null. Accepts number, US dollars, between 0 and 10000000.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 10000000,
              "x-unit": "usd",
              "examples": [
                1800
              ]
            }
          },
          {
            "name": "grossMonthlyIncome",
            "in": "query",
            "required": false,
            "description": "Monthly income **before** taxes. Used for nothing but HUD's cost-burden test, which is defined on gross income. Omit it and `housing.hud` comes back null rather than being estimated from take-home. Accepts number, US dollars, between 0 and 10000000.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 10000000,
              "x-unit": "usd",
              "examples": [
                6000
              ]
            }
          },
          {
            "name": "annualIncome",
            "in": "query",
            "required": false,
            "description": "Annual **pre-tax family** income. Selects `margin.band` only — it changes no arithmetic. Omit it and the band comes back null. Accepts number, US dollars, between 0 and 1000000000.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 1000000000,
              "x-unit": "usd",
              "examples": [
                72000
              ]
            }
          }
        ]
      },
      "post": {
        "tags": [
          "calculators"
        ],
        "summary": "50/30/20 budget",
        "description": "Take-home pay split into needs, wants and savings on the 50/30/20 rule — with housing measured against the needs slice it has to fit inside, and against the two benchmarks that are actually published.\n\n**50/30/20 is a rule of thumb from a book, not a standard.** It traces to the 2005 book **All Your Worth: The Ultimate Lifetime Money Plan**, by Elizabeth Warren and Amelia Warren Tyagi. No regulator publishes it, no agency measures against it, and no federal survey reports it as a norm. This endpoint implements it faithfully and returns `ruleOrigin` with every response, so a caller quoting the split can say where it came from instead of implying an authority that does not exist.\n\n**shareOfNeedsBudgetPct is the figure to quote, and nobody else publishes it.** Everyone reports housing as a share of income; the question the rule actually poses is whether housing fits inside the 50% it allows for **all** needs. On $4,500 of take-home the needs slice is $2,250, so $1,800 of rent is **80% of the entire needs budget** and leaves $450 for food, utilities, transport, insurance and minimum debt payments. Above 100 the rule is broken before groceries. That is the arithmetic answer to \"is 50/30/20 realistic?\".\n\n**HUD's 30% is defined on gross income, and this endpoint will not apply it to take-home pay.** HUD defines affordable housing as housing \"on which the occupant is paying no more than 30 percent of gross income for housing costs, including utilities\", above 50 percent being the severe case; its CHAS dataset counts cost-burdened households against exactly that threshold. Take-home pay is a smaller denominator, so the same rent scores higher against it — $1,800 is 40% of $4,500 take-home but 30.0% of $6,000 gross, which is one household and two different verdicts. `housing.hud` therefore stays null until `grossMonthlyIncome` arrives rather than guessing a gross figure. Sources: https://archives.hud.gov/local/nv/goodstories/2006-04-06glos.cfm and https://www.huduser.gov/portal/datasets/cp.html\n\n**Saving 20% is not the norm, and there is a measurement for it.** The Federal Reserve's Survey of Household Economics and Decisionmaking asks how often people have money left over at the end of the month. In 2025, **41 percent** of US adults said always or often — similar to 2024 — and it ranges from 19 percent under $25,000 of family income to 59 percent at $100,000 or more. The rule's savings slice is a target, and `margin` is what the population actually reports. https://www.federalreserve.gov/publications/2026-economic-well-being-of-us-households-in-2025-income-and-expenses.htm\n\nThe Board publishes whole percents, so the five-point distributions in `margin.bands` sum to between 99 and 101 rather than exactly 100. That is rounding in the source. `alwaysOrOftenPct` is the Board's own stated figure rather than a sum of two rounded components — which matters in the lowest band, where the prose says 19 and `often + always` sums to 20.\n\n`annualIncome` is pre-tax family income for the whole household, because that is the basis the Fed's bands use. `income` is monthly take-home for one budget. They are deliberately different questions and neither is derived from the other.\n\nNothing here is advice about what the split **should** be. The rule is returned as the rule, the two benchmarks are returned as measurements, and they are kept in separate objects so nothing borrows the other's authority.\n\nIdentical to the GET, with the parameters in a JSON object body.",
        "externalDocs": {
          "description": "The 50/30/20 budget calculator on farbetteroff.com",
          "url": "https://farbetteroff.com/calculators/budget-calculator"
        },
        "responses": {
          "200": {
            "$ref": "#/components/responses/budget-result"
          },
          "400": {
            "description": "A parameter is missing, mistyped, out of range or unknown — all of them are listed at once. A name that is one or two edits from a real parameter is answered with the one it probably meant; an ambiguous one is answered with the accepted list instead of a guess.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such endpoint. The response lists every valid slug.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "More than 60 requests in 60 seconds from one IP.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "A bug on our side. The inputs are never at fault for this one.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "operationId": "post-budget",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "income"
                ],
                "properties": {
                  "income": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 10000000,
                    "x-unit": "usd",
                    "examples": [
                      4500
                    ],
                    "description": "Monthly **take-home** pay — after taxes and payroll deductions. Accepts number, US dollars, between 0 and 10000000."
                  },
                  "housing": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 10000000,
                    "x-unit": "usd",
                    "examples": [
                      1800
                    ],
                    "description": "Monthly housing cost **including utilities**, HUD's basis. Drives the whole `housing` object; omit it and that comes back null. Accepts number, US dollars, between 0 and 10000000."
                  },
                  "grossMonthlyIncome": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 10000000,
                    "x-unit": "usd",
                    "examples": [
                      6000
                    ],
                    "description": "Monthly income **before** taxes. Used for nothing but HUD's cost-burden test, which is defined on gross income. Omit it and `housing.hud` comes back null rather than being estimated from take-home. Accepts number, US dollars, between 0 and 10000000."
                  },
                  "annualIncome": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 1000000000,
                    "x-unit": "usd",
                    "examples": [
                      72000
                    ],
                    "description": "Annual **pre-tax family** income. Selects `margin.band` only — it changes no arithmetic. Omit it and the band comes back null. Accepts number, US dollars, between 0 and 1000000000."
                  }
                }
              }
            }
          }
        }
      }
    },
    "/cd": {
      "get": {
        "tags": [
          "calculators"
        ],
        "summary": "Certificate of deposit",
        "description": "What a CD is worth at maturity, from the APY the bank quotes.\n\nAPY already accounts for the bank's compounding frequency, which is what makes it the comparable number (12 CFR Part 1030 / Regulation DD, Appendix A).\n\nInterest is shown before tax. CD interest is generally taxable as ordinary income.",
        "externalDocs": {
          "description": "The certificate of deposit calculator on farbetteroff.com",
          "url": "https://farbetteroff.com/calculators/cd-calculator"
        },
        "responses": {
          "200": {
            "$ref": "#/components/responses/cd-result"
          },
          "400": {
            "description": "A parameter is missing, mistyped, out of range or unknown — all of them are listed at once. A name that is one or two edits from a real parameter is answered with the one it probably meant; an ambiguous one is answered with the accepted list instead of a guess.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such endpoint. The response lists every valid slug.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "More than 60 requests in 60 seconds from one IP.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "A bug on our side. The inputs are never at fault for this one.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "operationId": "get-cd",
        "parameters": [
          {
            "name": "deposit",
            "in": "query",
            "required": true,
            "description": "Amount deposited. Accepts number, US dollars, between 0 and 10000000000.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 10000000000,
              "x-unit": "usd",
              "examples": [
                10000
              ]
            }
          },
          {
            "name": "apy",
            "in": "query",
            "required": true,
            "description": "Annual percentage yield, as quoted. Accepts number, a percentage, so 6.5 means 6.5%, between 0 and 50.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 50,
              "x-unit": "percent",
              "examples": [
                4.25
              ]
            }
          },
          {
            "name": "termMonths",
            "in": "query",
            "required": true,
            "description": "Term of the CD in months. Accepts integer, whole months, between 1 and 600.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 600,
              "x-unit": "months",
              "examples": [
                12
              ]
            }
          }
        ]
      },
      "post": {
        "tags": [
          "calculators"
        ],
        "summary": "Certificate of deposit",
        "description": "What a CD is worth at maturity, from the APY the bank quotes.\n\nAPY already accounts for the bank's compounding frequency, which is what makes it the comparable number (12 CFR Part 1030 / Regulation DD, Appendix A).\n\nInterest is shown before tax. CD interest is generally taxable as ordinary income.\n\nIdentical to the GET, with the parameters in a JSON object body.",
        "externalDocs": {
          "description": "The certificate of deposit calculator on farbetteroff.com",
          "url": "https://farbetteroff.com/calculators/cd-calculator"
        },
        "responses": {
          "200": {
            "$ref": "#/components/responses/cd-result"
          },
          "400": {
            "description": "A parameter is missing, mistyped, out of range or unknown — all of them are listed at once. A name that is one or two edits from a real parameter is answered with the one it probably meant; an ambiguous one is answered with the accepted list instead of a guess.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such endpoint. The response lists every valid slug.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "More than 60 requests in 60 seconds from one IP.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "A bug on our side. The inputs are never at fault for this one.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "operationId": "post-cd",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "deposit",
                  "apy",
                  "termMonths"
                ],
                "properties": {
                  "deposit": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 10000000000,
                    "x-unit": "usd",
                    "examples": [
                      10000
                    ],
                    "description": "Amount deposited. Accepts number, US dollars, between 0 and 10000000000."
                  },
                  "apy": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 50,
                    "x-unit": "percent",
                    "examples": [
                      4.25
                    ],
                    "description": "Annual percentage yield, as quoted. Accepts number, a percentage, so 6.5 means 6.5%, between 0 and 50."
                  },
                  "termMonths": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 600,
                    "x-unit": "months",
                    "examples": [
                      12
                    ],
                    "description": "Term of the CD in months. Accepts integer, whole months, between 1 and 600."
                  }
                }
              }
            }
          }
        }
      }
    },
    "/inflation": {
      "get": {
        "tags": [
          "calculators"
        ],
        "summary": "Inflation impact",
        "description": "What a sum of money will cost, and what it will be worth, after inflation has run for a while.",
        "externalDocs": {
          "description": "The inflation impact calculator on farbetteroff.com",
          "url": "https://farbetteroff.com/calculators/inflation-calculator"
        },
        "responses": {
          "200": {
            "$ref": "#/components/responses/inflation-result"
          },
          "400": {
            "description": "A parameter is missing, mistyped, out of range or unknown — all of them are listed at once. A name that is one or two edits from a real parameter is answered with the one it probably meant; an ambiguous one is answered with the accepted list instead of a guess.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such endpoint. The response lists every valid slug.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "More than 60 requests in 60 seconds from one IP.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "A bug on our side. The inputs are never at fault for this one.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "operationId": "get-inflation",
        "parameters": [
          {
            "name": "amount",
            "in": "query",
            "required": true,
            "description": "Today's amount. Accepts number, US dollars, between 0 and 1000000000000.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 1000000000000,
              "x-unit": "usd",
              "examples": [
                10000
              ]
            }
          },
          {
            "name": "years",
            "in": "query",
            "required": true,
            "description": "How many years forward. Accepts number, years, between 0 and 200.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 200,
              "x-unit": "years",
              "examples": [
                20
              ]
            }
          },
          {
            "name": "rate",
            "in": "query",
            "required": false,
            "description": "Average annual inflation rate. Accepts number, a percentage, so 6.5 means 6.5%, between 0 and 100.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 100,
              "default": 3,
              "x-unit": "percent",
              "examples": [
                3
              ]
            }
          }
        ]
      },
      "post": {
        "tags": [
          "calculators"
        ],
        "summary": "Inflation impact",
        "description": "What a sum of money will cost, and what it will be worth, after inflation has run for a while.\n\nIdentical to the GET, with the parameters in a JSON object body.",
        "externalDocs": {
          "description": "The inflation impact calculator on farbetteroff.com",
          "url": "https://farbetteroff.com/calculators/inflation-calculator"
        },
        "responses": {
          "200": {
            "$ref": "#/components/responses/inflation-result"
          },
          "400": {
            "description": "A parameter is missing, mistyped, out of range or unknown — all of them are listed at once. A name that is one or two edits from a real parameter is answered with the one it probably meant; an ambiguous one is answered with the accepted list instead of a guess.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such endpoint. The response lists every valid slug.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "More than 60 requests in 60 seconds from one IP.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "A bug on our side. The inputs are never at fault for this one.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "operationId": "post-inflation",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "amount",
                  "years"
                ],
                "properties": {
                  "amount": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 1000000000000,
                    "x-unit": "usd",
                    "examples": [
                      10000
                    ],
                    "description": "Today's amount. Accepts number, US dollars, between 0 and 1000000000000."
                  },
                  "years": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 200,
                    "x-unit": "years",
                    "examples": [
                      20
                    ],
                    "description": "How many years forward. Accepts number, years, between 0 and 200."
                  },
                  "rate": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 100,
                    "default": 3,
                    "x-unit": "percent",
                    "examples": [
                      3
                    ],
                    "description": "Average annual inflation rate. Accepts number, a percentage, so 6.5 means 6.5%, between 0 and 100."
                  }
                }
              }
            }
          }
        }
      }
    },
    "/401k": {
      "get": {
        "tags": [
          "calculators"
        ],
        "summary": "401(k) projection",
        "description": "A 401(k) balance projected to retirement, split into your money, the employer's money and growth.\n\n2026 employee deferral limit: $24,500 (IRS Notice 2025-67), $32,500 from the year you turn 50 and $35,750 for the four years you turn 60 to 63 — send currentAge to have each year use its own. The 2026 figures apply to every projected year; they rise with inflation most years.\n\nA projection, not a promise: it assumes a constant return and a constant raise, and markets do neither.",
        "externalDocs": {
          "description": "The 401(k) projection calculator on farbetteroff.com",
          "url": "https://farbetteroff.com/calculators/401k-calculator"
        },
        "responses": {
          "200": {
            "$ref": "#/components/responses/401k-result"
          },
          "400": {
            "description": "A parameter is missing, mistyped, out of range or unknown — all of them are listed at once. A name that is one or two edits from a real parameter is answered with the one it probably meant; an ambiguous one is answered with the accepted list instead of a guess.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such endpoint. The response lists every valid slug.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "More than 60 requests in 60 seconds from one IP.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "A bug on our side. The inputs are never at fault for this one.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "operationId": "get-401k",
        "parameters": [
          {
            "name": "currentBalance",
            "in": "query",
            "required": false,
            "description": "What is in the account today. Accepts number, US dollars, between 0 and 10000000000.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 10000000000,
              "default": 0,
              "x-unit": "usd",
              "examples": [
                25000
              ]
            }
          },
          {
            "name": "salary",
            "in": "query",
            "required": true,
            "description": "Current annual salary. Accepts number, US dollars, between 0 and 1000000000.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 1000000000,
              "x-unit": "usd",
              "examples": [
                65000
              ]
            }
          },
          {
            "name": "contribPercent",
            "in": "query",
            "required": true,
            "description": "Share of salary you defer each year. Accepts number, a percentage, so 6.5 means 6.5%, between 0 and 100.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 100,
              "x-unit": "percent",
              "examples": [
                6
              ]
            }
          },
          {
            "name": "matchRatePercent",
            "in": "query",
            "required": false,
            "description": "Cents on the dollar the employer matches — 50 means 50%. Accepts number, a percentage, so 6.5 means 6.5%, between 0 and 200.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 200,
              "default": 50,
              "x-unit": "percent"
            }
          },
          {
            "name": "matchLimitPercent",
            "in": "query",
            "required": false,
            "description": "Share of salary the match applies up to. Accepts number, a percentage, so 6.5 means 6.5%, between 0 and 100.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 100,
              "default": 6,
              "x-unit": "percent"
            }
          },
          {
            "name": "rate",
            "in": "query",
            "required": false,
            "description": "Average annual return. Accepts number, a percentage, so 6.5 means 6.5%, between 0 and 100.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 100,
              "default": 7,
              "x-unit": "percent"
            }
          },
          {
            "name": "annualRaisePercent",
            "in": "query",
            "required": false,
            "description": "Average yearly raise. Accepts number, a percentage, so 6.5 means 6.5%, between 0 and 50.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 50,
              "default": 2,
              "x-unit": "percent"
            }
          },
          {
            "name": "years",
            "in": "query",
            "required": true,
            "description": "Years until you stop contributing. Accepts integer, years, between 0 and 70.",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "maximum": 70,
              "x-unit": "years",
              "examples": [
                35
              ]
            }
          },
          {
            "name": "currentAge",
            "in": "query",
            "required": false,
            "description": "The age you reach this year. Given it, each year of the projection uses that year's own § 402(g) limit, including the age-50 and age-60-to-63 catch-ups. Ignored when deferralLimit is sent. Accepts integer, years, between 0 and 100.",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "maximum": 100,
              "x-unit": "years"
            }
          },
          {
            "name": "deferralLimit",
            "in": "query",
            "required": false,
            "description": "Employee deferral cap for every year, overriding currentAge. Omit both for the flat 2026 limit, $24,500. Accepts number, US dollars, between 0 and 1000000.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 1000000,
              "x-unit": "usd"
            }
          },
          {
            "name": "series",
            "in": "query",
            "required": false,
            "description": "Include the year-by-year balance series. Accepts boolean.",
            "schema": {
              "type": "boolean",
              "default": false
            }
          }
        ]
      },
      "post": {
        "tags": [
          "calculators"
        ],
        "summary": "401(k) projection",
        "description": "A 401(k) balance projected to retirement, split into your money, the employer's money and growth.\n\n2026 employee deferral limit: $24,500 (IRS Notice 2025-67), $32,500 from the year you turn 50 and $35,750 for the four years you turn 60 to 63 — send currentAge to have each year use its own. The 2026 figures apply to every projected year; they rise with inflation most years.\n\nA projection, not a promise: it assumes a constant return and a constant raise, and markets do neither.\n\nIdentical to the GET, with the parameters in a JSON object body.",
        "externalDocs": {
          "description": "The 401(k) projection calculator on farbetteroff.com",
          "url": "https://farbetteroff.com/calculators/401k-calculator"
        },
        "responses": {
          "200": {
            "$ref": "#/components/responses/401k-result"
          },
          "400": {
            "description": "A parameter is missing, mistyped, out of range or unknown — all of them are listed at once. A name that is one or two edits from a real parameter is answered with the one it probably meant; an ambiguous one is answered with the accepted list instead of a guess.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such endpoint. The response lists every valid slug.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "More than 60 requests in 60 seconds from one IP.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "A bug on our side. The inputs are never at fault for this one.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "operationId": "post-401k",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "salary",
                  "contribPercent",
                  "years"
                ],
                "properties": {
                  "currentBalance": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 10000000000,
                    "default": 0,
                    "x-unit": "usd",
                    "examples": [
                      25000
                    ],
                    "description": "What is in the account today. Accepts number, US dollars, between 0 and 10000000000."
                  },
                  "salary": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 1000000000,
                    "x-unit": "usd",
                    "examples": [
                      65000
                    ],
                    "description": "Current annual salary. Accepts number, US dollars, between 0 and 1000000000."
                  },
                  "contribPercent": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 100,
                    "x-unit": "percent",
                    "examples": [
                      6
                    ],
                    "description": "Share of salary you defer each year. Accepts number, a percentage, so 6.5 means 6.5%, between 0 and 100."
                  },
                  "matchRatePercent": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 200,
                    "default": 50,
                    "x-unit": "percent",
                    "description": "Cents on the dollar the employer matches — 50 means 50%. Accepts number, a percentage, so 6.5 means 6.5%, between 0 and 200."
                  },
                  "matchLimitPercent": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 100,
                    "default": 6,
                    "x-unit": "percent",
                    "description": "Share of salary the match applies up to. Accepts number, a percentage, so 6.5 means 6.5%, between 0 and 100."
                  },
                  "rate": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 100,
                    "default": 7,
                    "x-unit": "percent",
                    "description": "Average annual return. Accepts number, a percentage, so 6.5 means 6.5%, between 0 and 100."
                  },
                  "annualRaisePercent": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 50,
                    "default": 2,
                    "x-unit": "percent",
                    "description": "Average yearly raise. Accepts number, a percentage, so 6.5 means 6.5%, between 0 and 50."
                  },
                  "years": {
                    "type": "integer",
                    "minimum": 0,
                    "maximum": 70,
                    "x-unit": "years",
                    "examples": [
                      35
                    ],
                    "description": "Years until you stop contributing. Accepts integer, years, between 0 and 70."
                  },
                  "currentAge": {
                    "type": "integer",
                    "minimum": 0,
                    "maximum": 100,
                    "x-unit": "years",
                    "description": "The age you reach this year. Given it, each year of the projection uses that year's own § 402(g) limit, including the age-50 and age-60-to-63 catch-ups. Ignored when deferralLimit is sent. Accepts integer, years, between 0 and 100."
                  },
                  "deferralLimit": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 1000000,
                    "x-unit": "usd",
                    "description": "Employee deferral cap for every year, overriding currentAge. Omit both for the flat 2026 limit, $24,500. Accepts number, US dollars, between 0 and 1000000."
                  },
                  "series": {
                    "type": "boolean",
                    "default": false,
                    "description": "Include the year-by-year balance series. Accepts boolean."
                  }
                }
              }
            }
          }
        }
      }
    },
    "/401k-match": {
      "get": {
        "tags": [
          "calculators"
        ],
        "summary": "Employer match",
        "description": "What your employer's match is worth this year — and how much of it you are leaving on the table.",
        "externalDocs": {
          "description": "The employer match calculator on farbetteroff.com",
          "url": "https://farbetteroff.com/calculators/401k-match-calculator"
        },
        "responses": {
          "200": {
            "$ref": "#/components/responses/401k-match-result"
          },
          "400": {
            "description": "A parameter is missing, mistyped, out of range or unknown — all of them are listed at once. A name that is one or two edits from a real parameter is answered with the one it probably meant; an ambiguous one is answered with the accepted list instead of a guess.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such endpoint. The response lists every valid slug.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "More than 60 requests in 60 seconds from one IP.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "A bug on our side. The inputs are never at fault for this one.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "operationId": "get-401k-match",
        "parameters": [
          {
            "name": "salary",
            "in": "query",
            "required": true,
            "description": "Annual salary. Accepts number, US dollars, between 0 and 1000000000.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 1000000000,
              "x-unit": "usd",
              "examples": [
                60000
              ]
            }
          },
          {
            "name": "contribPercent",
            "in": "query",
            "required": true,
            "description": "Share of salary you contribute. Accepts number, a percentage, so 6.5 means 6.5%, between 0 and 100.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 100,
              "x-unit": "percent",
              "examples": [
                4
              ]
            }
          },
          {
            "name": "matchRatePercent",
            "in": "query",
            "required": false,
            "description": "Cents on the dollar matched — 50 means 50%, 100 means dollar for dollar. Accepts number, a percentage, so 6.5 means 6.5%, between 0 and 200.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 200,
              "default": 50,
              "x-unit": "percent",
              "examples": [
                50
              ]
            }
          },
          {
            "name": "matchLimitPercent",
            "in": "query",
            "required": false,
            "description": "Share of salary the match applies up to. Accepts number, a percentage, so 6.5 means 6.5%, between 0 and 100.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 100,
              "default": 6,
              "x-unit": "percent",
              "examples": [
                6
              ]
            }
          }
        ]
      },
      "post": {
        "tags": [
          "calculators"
        ],
        "summary": "Employer match",
        "description": "What your employer's match is worth this year — and how much of it you are leaving on the table.\n\nIdentical to the GET, with the parameters in a JSON object body.",
        "externalDocs": {
          "description": "The employer match calculator on farbetteroff.com",
          "url": "https://farbetteroff.com/calculators/401k-match-calculator"
        },
        "responses": {
          "200": {
            "$ref": "#/components/responses/401k-match-result"
          },
          "400": {
            "description": "A parameter is missing, mistyped, out of range or unknown — all of them are listed at once. A name that is one or two edits from a real parameter is answered with the one it probably meant; an ambiguous one is answered with the accepted list instead of a guess.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such endpoint. The response lists every valid slug.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "More than 60 requests in 60 seconds from one IP.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "A bug on our side. The inputs are never at fault for this one.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "operationId": "post-401k-match",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "salary",
                  "contribPercent"
                ],
                "properties": {
                  "salary": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 1000000000,
                    "x-unit": "usd",
                    "examples": [
                      60000
                    ],
                    "description": "Annual salary. Accepts number, US dollars, between 0 and 1000000000."
                  },
                  "contribPercent": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 100,
                    "x-unit": "percent",
                    "examples": [
                      4
                    ],
                    "description": "Share of salary you contribute. Accepts number, a percentage, so 6.5 means 6.5%, between 0 and 100."
                  },
                  "matchRatePercent": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 200,
                    "default": 50,
                    "x-unit": "percent",
                    "examples": [
                      50
                    ],
                    "description": "Cents on the dollar matched — 50 means 50%, 100 means dollar for dollar. Accepts number, a percentage, so 6.5 means 6.5%, between 0 and 200."
                  },
                  "matchLimitPercent": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 100,
                    "default": 6,
                    "x-unit": "percent",
                    "examples": [
                      6
                    ],
                    "description": "Share of salary the match applies up to. Accepts number, a percentage, so 6.5 means 6.5%, between 0 and 100."
                  }
                }
              }
            }
          }
        }
      }
    },
    "/safe-withdrawal": {
      "get": {
        "tags": [
          "calculators"
        ],
        "summary": "Safe withdrawal rate",
        "description": "The income a nest egg supports — the 4% rule, and the 25× multiple behind it.\n\nThe 4% rule comes from William Bengen's 1994 study and the 1998 Trinity Study: a 4% inflation-adjusted first-year withdrawal survived every 30-year period in US market history. 30 years is the worst case in that research, not a guarantee.",
        "externalDocs": {
          "description": "The safe withdrawal rate calculator on farbetteroff.com",
          "url": "https://farbetteroff.com/calculators/retirement-calculator"
        },
        "responses": {
          "200": {
            "$ref": "#/components/responses/safe-withdrawal-result"
          },
          "400": {
            "description": "A parameter is missing, mistyped, out of range or unknown — all of them are listed at once. A name that is one or two edits from a real parameter is answered with the one it probably meant; an ambiguous one is answered with the accepted list instead of a guess.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such endpoint. The response lists every valid slug.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "More than 60 requests in 60 seconds from one IP.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "A bug on our side. The inputs are never at fault for this one.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "operationId": "get-safe-withdrawal",
        "parameters": [
          {
            "name": "nestEgg",
            "in": "query",
            "required": true,
            "description": "Portfolio value at retirement. Accepts number, US dollars, between 0 and 1000000000000.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 1000000000000,
              "x-unit": "usd",
              "examples": [
                1000000
              ]
            }
          },
          {
            "name": "withdrawalRate",
            "in": "query",
            "required": false,
            "description": "First-year withdrawal rate. Accepts number, a percentage, so 6.5 means 6.5%, between 0.1 and 20.",
            "schema": {
              "type": "number",
              "minimum": 0.1,
              "maximum": 20,
              "default": 4,
              "x-unit": "percent",
              "examples": [
                4
              ]
            }
          }
        ]
      },
      "post": {
        "tags": [
          "calculators"
        ],
        "summary": "Safe withdrawal rate",
        "description": "The income a nest egg supports — the 4% rule, and the 25× multiple behind it.\n\nThe 4% rule comes from William Bengen's 1994 study and the 1998 Trinity Study: a 4% inflation-adjusted first-year withdrawal survived every 30-year period in US market history. 30 years is the worst case in that research, not a guarantee.\n\nIdentical to the GET, with the parameters in a JSON object body.",
        "externalDocs": {
          "description": "The safe withdrawal rate calculator on farbetteroff.com",
          "url": "https://farbetteroff.com/calculators/retirement-calculator"
        },
        "responses": {
          "200": {
            "$ref": "#/components/responses/safe-withdrawal-result"
          },
          "400": {
            "description": "A parameter is missing, mistyped, out of range or unknown — all of them are listed at once. A name that is one or two edits from a real parameter is answered with the one it probably meant; an ambiguous one is answered with the accepted list instead of a guess.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such endpoint. The response lists every valid slug.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "More than 60 requests in 60 seconds from one IP.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "A bug on our side. The inputs are never at fault for this one.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "operationId": "post-safe-withdrawal",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "nestEgg"
                ],
                "properties": {
                  "nestEgg": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 1000000000000,
                    "x-unit": "usd",
                    "examples": [
                      1000000
                    ],
                    "description": "Portfolio value at retirement. Accepts number, US dollars, between 0 and 1000000000000."
                  },
                  "withdrawalRate": {
                    "type": "number",
                    "minimum": 0.1,
                    "maximum": 20,
                    "default": 4,
                    "x-unit": "percent",
                    "examples": [
                      4
                    ],
                    "description": "First-year withdrawal rate. Accepts number, a percentage, so 6.5 means 6.5%, between 0.1 and 20."
                  }
                }
              }
            }
          }
        }
      }
    },
    "/fire": {
      "get": {
        "tags": [
          "calculators"
        ],
        "summary": "FIRE number and date",
        "description": "The portfolio that covers your spending forever, and how many years of saving it takes to get there.\n\nBecause the rate is a real return, no separate inflation adjustment is applied — every figure is in today's dollars.",
        "externalDocs": {
          "description": "The fire number and date calculator on farbetteroff.com",
          "url": "https://farbetteroff.com/calculators/fire-calculator"
        },
        "responses": {
          "200": {
            "$ref": "#/components/responses/fire-result"
          },
          "400": {
            "description": "A parameter is missing, mistyped, out of range or unknown — all of them are listed at once. A name that is one or two edits from a real parameter is answered with the one it probably meant; an ambiguous one is answered with the accepted list instead of a guess.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such endpoint. The response lists every valid slug.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "More than 60 requests in 60 seconds from one IP.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "A bug on our side. The inputs are never at fault for this one.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "operationId": "get-fire",
        "parameters": [
          {
            "name": "annualSpending",
            "in": "query",
            "required": true,
            "description": "What you expect to spend per year in retirement. Accepts number, US dollars, between 0 and 1000000000.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 1000000000,
              "x-unit": "usd",
              "examples": [
                45000
              ]
            }
          },
          {
            "name": "current",
            "in": "query",
            "required": false,
            "description": "Invested savings today. Accepts number, US dollars, between 0 and 1000000000000.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 1000000000000,
              "default": 0,
              "x-unit": "usd",
              "examples": [
                30000
              ]
            }
          },
          {
            "name": "annualSavings",
            "in": "query",
            "required": false,
            "description": "What you invest per year. Accepts number, US dollars, between 0 and 1000000000.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 1000000000,
              "default": 0,
              "x-unit": "usd",
              "examples": [
                25000
              ]
            }
          },
          {
            "name": "rate",
            "in": "query",
            "required": false,
            "description": "Real (after-inflation) annual return, so the answer is already in today's dollars. Accepts number, a percentage, so 6.5 means 6.5%, between 0 and 100.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 100,
              "default": 7,
              "x-unit": "percent",
              "examples": [
                7
              ]
            }
          },
          {
            "name": "withdrawalRate",
            "in": "query",
            "required": false,
            "description": "Withdrawal rate used to set the target. Accepts number, a percentage, so 6.5 means 6.5%, between 0.1 and 20.",
            "schema": {
              "type": "number",
              "minimum": 0.1,
              "maximum": 20,
              "default": 4,
              "x-unit": "percent"
            }
          }
        ]
      },
      "post": {
        "tags": [
          "calculators"
        ],
        "summary": "FIRE number and date",
        "description": "The portfolio that covers your spending forever, and how many years of saving it takes to get there.\n\nBecause the rate is a real return, no separate inflation adjustment is applied — every figure is in today's dollars.\n\nIdentical to the GET, with the parameters in a JSON object body.",
        "externalDocs": {
          "description": "The fire number and date calculator on farbetteroff.com",
          "url": "https://farbetteroff.com/calculators/fire-calculator"
        },
        "responses": {
          "200": {
            "$ref": "#/components/responses/fire-result"
          },
          "400": {
            "description": "A parameter is missing, mistyped, out of range or unknown — all of them are listed at once. A name that is one or two edits from a real parameter is answered with the one it probably meant; an ambiguous one is answered with the accepted list instead of a guess.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such endpoint. The response lists every valid slug.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "More than 60 requests in 60 seconds from one IP.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "A bug on our side. The inputs are never at fault for this one.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "operationId": "post-fire",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "annualSpending"
                ],
                "properties": {
                  "annualSpending": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 1000000000,
                    "x-unit": "usd",
                    "examples": [
                      45000
                    ],
                    "description": "What you expect to spend per year in retirement. Accepts number, US dollars, between 0 and 1000000000."
                  },
                  "current": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 1000000000000,
                    "default": 0,
                    "x-unit": "usd",
                    "examples": [
                      30000
                    ],
                    "description": "Invested savings today. Accepts number, US dollars, between 0 and 1000000000000."
                  },
                  "annualSavings": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 1000000000,
                    "default": 0,
                    "x-unit": "usd",
                    "examples": [
                      25000
                    ],
                    "description": "What you invest per year. Accepts number, US dollars, between 0 and 1000000000."
                  },
                  "rate": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 100,
                    "default": 7,
                    "x-unit": "percent",
                    "examples": [
                      7
                    ],
                    "description": "Real (after-inflation) annual return, so the answer is already in today's dollars. Accepts number, a percentage, so 6.5 means 6.5%, between 0 and 100."
                  },
                  "withdrawalRate": {
                    "type": "number",
                    "minimum": 0.1,
                    "maximum": 20,
                    "default": 4,
                    "x-unit": "percent",
                    "description": "Withdrawal rate used to set the target. Accepts number, a percentage, so 6.5 means 6.5%, between 0.1 and 20."
                  }
                }
              }
            }
          }
        }
      }
    },
    "/coast-fire": {
      "get": {
        "tags": [
          "calculators"
        ],
        "summary": "Coast FIRE",
        "description": "The amount that, invested today, grows into your FIRE number by retirement with no further contributions.",
        "externalDocs": {
          "description": "The coast fire calculator on farbetteroff.com",
          "url": "https://farbetteroff.com/calculators/fire-calculator"
        },
        "responses": {
          "200": {
            "$ref": "#/components/responses/coast-fire-result"
          },
          "400": {
            "description": "A parameter is missing, mistyped, out of range or unknown — all of them are listed at once. A name that is one or two edits from a real parameter is answered with the one it probably meant; an ambiguous one is answered with the accepted list instead of a guess.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such endpoint. The response lists every valid slug.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "More than 60 requests in 60 seconds from one IP.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "A bug on our side. The inputs are never at fault for this one.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "operationId": "get-coast-fire",
        "parameters": [
          {
            "name": "annualSpending",
            "in": "query",
            "required": true,
            "description": "Expected annual spending in retirement. Accepts number, US dollars, between 0 and 1000000000.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 1000000000,
              "x-unit": "usd",
              "examples": [
                45000
              ]
            }
          },
          {
            "name": "current",
            "in": "query",
            "required": false,
            "description": "Invested savings today. Accepts number, US dollars, between 0 and 1000000000000.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 1000000000000,
              "default": 0,
              "x-unit": "usd",
              "examples": [
                30000
              ]
            }
          },
          {
            "name": "annualSavings",
            "in": "query",
            "required": false,
            "description": "What you invest per year until you coast. Accepts number, US dollars, between 0 and 1000000000.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 1000000000,
              "default": 0,
              "x-unit": "usd",
              "examples": [
                25000
              ]
            }
          },
          {
            "name": "yearsToRetirement",
            "in": "query",
            "required": true,
            "description": "Years until you plan to retire. Accepts integer, years, between 0 and 70.",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "maximum": 70,
              "x-unit": "years",
              "examples": [
                35
              ]
            }
          },
          {
            "name": "rate",
            "in": "query",
            "required": false,
            "description": "Real (after-inflation) annual return. Accepts number, a percentage, so 6.5 means 6.5%, between 0 and 100.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 100,
              "default": 7,
              "x-unit": "percent"
            }
          },
          {
            "name": "withdrawalRate",
            "in": "query",
            "required": false,
            "description": "Withdrawal rate used to set the target. Accepts number, a percentage, so 6.5 means 6.5%, between 0.1 and 20.",
            "schema": {
              "type": "number",
              "minimum": 0.1,
              "maximum": 20,
              "default": 4,
              "x-unit": "percent"
            }
          }
        ]
      },
      "post": {
        "tags": [
          "calculators"
        ],
        "summary": "Coast FIRE",
        "description": "The amount that, invested today, grows into your FIRE number by retirement with no further contributions.\n\nIdentical to the GET, with the parameters in a JSON object body.",
        "externalDocs": {
          "description": "The coast fire calculator on farbetteroff.com",
          "url": "https://farbetteroff.com/calculators/fire-calculator"
        },
        "responses": {
          "200": {
            "$ref": "#/components/responses/coast-fire-result"
          },
          "400": {
            "description": "A parameter is missing, mistyped, out of range or unknown — all of them are listed at once. A name that is one or two edits from a real parameter is answered with the one it probably meant; an ambiguous one is answered with the accepted list instead of a guess.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such endpoint. The response lists every valid slug.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "More than 60 requests in 60 seconds from one IP.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "A bug on our side. The inputs are never at fault for this one.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "operationId": "post-coast-fire",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "annualSpending",
                  "yearsToRetirement"
                ],
                "properties": {
                  "annualSpending": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 1000000000,
                    "x-unit": "usd",
                    "examples": [
                      45000
                    ],
                    "description": "Expected annual spending in retirement. Accepts number, US dollars, between 0 and 1000000000."
                  },
                  "current": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 1000000000000,
                    "default": 0,
                    "x-unit": "usd",
                    "examples": [
                      30000
                    ],
                    "description": "Invested savings today. Accepts number, US dollars, between 0 and 1000000000000."
                  },
                  "annualSavings": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 1000000000,
                    "default": 0,
                    "x-unit": "usd",
                    "examples": [
                      25000
                    ],
                    "description": "What you invest per year until you coast. Accepts number, US dollars, between 0 and 1000000000."
                  },
                  "yearsToRetirement": {
                    "type": "integer",
                    "minimum": 0,
                    "maximum": 70,
                    "x-unit": "years",
                    "examples": [
                      35
                    ],
                    "description": "Years until you plan to retire. Accepts integer, years, between 0 and 70."
                  },
                  "rate": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 100,
                    "default": 7,
                    "x-unit": "percent",
                    "description": "Real (after-inflation) annual return. Accepts number, a percentage, so 6.5 means 6.5%, between 0 and 100."
                  },
                  "withdrawalRate": {
                    "type": "number",
                    "minimum": 0.1,
                    "maximum": 20,
                    "default": 4,
                    "x-unit": "percent",
                    "description": "Withdrawal rate used to set the target. Accepts number, a percentage, so 6.5 means 6.5%, between 0.1 and 20."
                  }
                }
              }
            }
          }
        }
      }
    },
    "/roth-vs-traditional": {
      "get": {
        "tags": [
          "calculators"
        ],
        "summary": "Roth vs Traditional",
        "description": "Which account leaves you with more after tax, compared at the same cost to you today — plus the effective federal rate a Traditional withdrawal actually pays, which is neither the bracket everyone quotes nor the 20% a plan withholds.\n\n**The retirement rate people quote is a bracket, and a withdrawal is not taxed at a bracket.** A Traditional withdrawal is ordinary income filling the brackets from the bottom — the standard deduction first at 0%, then 10%, then 12%. On $60,000 withdrawn by a single filer with no other taxable income, the marginal bracket is **12%** and the effective federal rate is **8.37%**. Someone comparing \"22% now against 22% later\" and concluding it is a wash has the later figure wrong by more than the decision is worth. Send `withdrawal` and use the `effectiveRatePct` that comes back as `taxRetire`.\n\n**That rate is a floor, not a forecast, and this endpoint says so in the response.** It assumes the withdrawal is the only ordinary income that year. A pension, a spouse's wages, interest, and required distributions from any other pre-tax account all stack underneath it and push it into higher bands — the same $60,000 on top of $40,000 of other income pays **17.58%**, more than double. That is what `otherTaxableIncome` is for, and why `assumesOnlyIncome` is returned rather than assumed.\n\n**The percentage a plan withheld is not a tax rate at all, and it is usually too much.** A retirement plan distribution paid to you carries mandatory withholding of 20% — the IRS's words are \"even if you intend to roll it over later\" — so it is a deposit against a bill computed months later, identical for everyone and knowing nothing about the filer. On the $60,000 above, the plan sends $12,000 against $5,020 owed: $6,980 lent to the Treasury, earning nothing, until the return is filed. The `withholding` block measures that gap, and `breakEvenWithdrawal` says where it closes — about $239,133 withdrawn in one year by that filer, which is why over-withholding is the normal case rather than an edge case. An IRA is the contrast that proves it is a default and not a rate: 10%, and electable out — send `account=ira` for it. https://www.irs.gov/retirement-plans/plan-participant-employee/rollovers-of-retirement-plan-and-ira-distributions\n\n**The 10% before 59½ is charged on the distribution, not on the tax** — which makes it larger than the surcharge people picture. Send `age` and `ageRules` prices it: the same $60,000 taken at 50 owes $5,020 of income tax and $6,000 of additional tax — the penalty is the **bigger** of the two — for an all-in federal rate of 18.37% against the 8.37% the same withdrawal costs after 59½. Because it lands on the distribution it adds exactly its own 10 points to the rate, whatever the rate was; a withdrawal small enough to owe no income tax at all still owes it. **The 72(t) exceptions are not modelled** — disability, death, substantially equal periodic payments, qualified birth or adoption, certain medical expenses, a domestic-abuse distribution, separation from service at 55 or later for a workplace plan — so `penalty` is what applies if none of them fits, which is what `assumesNoException` says out loud. A rollover is not an early distribution: the additional tax reaches only the taxable part. https://www.irs.gov/taxtopics/tc558\n\n**RMDs are reported, not computed.** `ageRules` answers whether you have reached 73 and how many years are left, because the amount depends on a life-expectancy table and the prior 31 December balance that this endpoint does not ask for. Two timing notes that are this endpoint's own arithmetic applied to a deadline: the first RMD can be deferred to April 1 of the following year, which stacks two distributions into one calendar year and raises the effective rate on both, and a participant still working for the employer sponsoring the plan can generally delay plan RMDs until they retire unless they own 5% or more of the business. Roth IRAs have no RMDs while the owner is alive. https://www.irs.gov/retirement-plans/retirement-plan-and-ira-required-minimum-distributions-faqs\n\n**Social Security benefit taxation is not modelled**, and it is the largest thing missing. A Traditional withdrawal can raise the share of a benefit that becomes taxable, which is a second effect on top of the tax on the withdrawal itself. A retiree drawing Social Security should read `retirementRate` as an understatement.\n\n**At the same tax rate now and later the two accounts are mathematically identical**, and this endpoint returns a tie rather than a winner. `(1 − t)·(1 + r)ⁿ` and `(1 + r)ⁿ·(1 − t)` are the same number. Any calculator that shows the Roth winning at equal rates is comparing equal **contributions** rather than equal cost — which compares a larger sacrifice against a smaller one. See `contribution` above.\n\n**The cap is the one argument for the Roth that does not depend on predicting tax rates.** $7,500 into a Roth shelters $7,500 of after-tax money; the same nominal amount into a Traditional costs only $5,850 after tax at a 22% rate. Matching the Roth would take $9,615 of pre-tax money, which the cap does not allow. `limitEdge` reports that gap. The deferred tax is real and stays in your pocket, but it lands in a taxable account whose drag this endpoint does not model, so it is returned as `taxSavings` and not projected.\n\n**Federal only, and not a tax return.** No state income tax (see `/api/v1/state-tax`), no IRMAA, no Net Investment Income Tax, no credits, no age-65 additional standard deduction, and no RMD schedule. Figures are for tax year 2026: standard deduction and brackets from IRS Rev. Proc. 2025-32, limits from IRS Notice 2025-67.\n\n**The eligibility object is the Roth contribution limit only.** Whether a **Traditional** contribution is **deductible** phases out on different ranges that also depend on whether you or a spouse are covered by a workplace plan, and that is not modelled here — an undeducted Traditional contribution changes the comparison completely, so do not read a `full` band as advice about the other account. Roth phase-out ranges for 2026: $153,000–$168,000 single and head of household, $242,000–$252,000 married filing jointly, and $0–$10,000 filing separately, which never adjusts for inflation. https://www.irs.gov/newsroom/401k-limit-increases-to-24500-for-2026-ira-limit-increases-to-7500\n\nNothing here is advice about which account to choose. The comparison is arithmetic on the rates you supply, and the rate that matters most is the one this endpoint computes rather than asks you to guess.",
        "externalDocs": {
          "description": "The roth vs traditional calculator on farbetteroff.com",
          "url": "https://farbetteroff.com/calculators/roth-vs-traditional-calculator"
        },
        "responses": {
          "200": {
            "$ref": "#/components/responses/roth-vs-traditional-result"
          },
          "400": {
            "description": "A parameter is missing, mistyped, out of range or unknown — all of them are listed at once. A name that is one or two edits from a real parameter is answered with the one it probably meant; an ambiguous one is answered with the accepted list instead of a guess.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such endpoint. The response lists every valid slug.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "More than 60 requests in 60 seconds from one IP.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "A bug on our side. The inputs are never at fault for this one.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "operationId": "get-roth-vs-traditional",
        "parameters": [
          {
            "name": "contribution",
            "in": "query",
            "required": false,
            "description": "Yearly contribution, measured as **what leaves your pocket after tax**. Defaults to the 2026 IRA limit. See the notes — this is not the same as the figure you type into a brokerage form. Accepts number, US dollars, between 0 and 100000.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 100000,
              "default": 7500,
              "x-unit": "usd"
            }
          },
          {
            "name": "years",
            "in": "query",
            "required": false,
            "description": "Years of contributions and growth before you start withdrawing. Accepts integer, years, between 1 and 60.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 60,
              "default": 30,
              "x-unit": "years",
              "examples": [
                30
              ]
            }
          },
          {
            "name": "rate",
            "in": "query",
            "required": false,
            "description": "Annual return, the same for both accounts. Accepts number, a percentage, so 6.5 means 6.5%, between 0 and 20.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 20,
              "default": 7,
              "x-unit": "percent"
            }
          },
          {
            "name": "taxNow",
            "in": "query",
            "required": true,
            "description": "Your marginal tax rate today — the bracket the contribution would otherwise be taxed in. Accepts number, a percentage, so 6.5 means 6.5%, between 0 and 100.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 100,
              "x-unit": "percent",
              "examples": [
                22
              ]
            }
          },
          {
            "name": "taxRetire",
            "in": "query",
            "required": true,
            "description": "The rate you expect the Traditional withdrawal to pay. **Send the effective rate, not a bracket** — `withdrawal` below computes it for you. Accepts number, a percentage, so 6.5 means 6.5%, between 0 and 100.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 100,
              "x-unit": "percent",
              "examples": [
                22
              ]
            }
          },
          {
            "name": "withdrawal",
            "in": "query",
            "required": false,
            "description": "A year's planned withdrawal from the pre-tax account. Drives the whole `retirementRate` object, which is the point of this endpoint; omit it and that comes back null. Accepts number, US dollars, between 0 and 10000000.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 10000000,
              "x-unit": "usd",
              "examples": [
                60000
              ]
            }
          },
          {
            "name": "otherTaxableIncome",
            "in": "query",
            "required": false,
            "description": "Other ordinary taxable income in the same retirement year — a pension, a spouse's wages, another account's required distribution. The withdrawal stacks on top of it, so this raises the rate. Social Security is not modelled; see the notes. Accepts number, US dollars, between 0 and 10000000.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 10000000,
              "default": 0,
              "x-unit": "usd"
            }
          },
          {
            "name": "withheldPct",
            "in": "query",
            "required": false,
            "description": "What the payer actually withheld, if you have the statement in front of you. Omit it and `withholding` uses the default for `account` — 20% or 10% — and flags it as assumed. Withholding is a deposit, not a rate; it changes no arithmetic above. Accepts number, a percentage, so 6.5 means 6.5%, between 0 and 100.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 100,
              "x-unit": "percent"
            }
          },
          {
            "name": "account",
            "in": "query",
            "required": false,
            "description": "Where the withdrawal is paid from, which sets the default withholding when `withheldPct` is omitted. A workplace plan withholds 20% and you cannot decline it; an IRA withholds 10% and you can elect out. Selects a default only — it changes no arithmetic. Accepts one of plan, ira. 8 of the listed spellings are aliases: they are resolved to one of 2 canonical values (`x-canonical-values`) before the answer echoes them. Matching ignores case.",
            "schema": {
              "type": "string",
              "enum": [
                "plan",
                "ira",
                "401k",
                "401(k)",
                "403b",
                "457b",
                "employer",
                "workplace",
                "traditional ira",
                "rollover ira"
              ],
              "x-canonical-values": [
                "plan",
                "ira"
              ],
              "default": "plan"
            }
          },
          {
            "name": "age",
            "in": "query",
            "required": false,
            "description": "Your age **when the withdrawal is taken**, which is a different age from `age50Plus` below — that one is about this year's contribution. Halves count, because the threshold is one: 59.4 is early and 59.5 is not. Selects `ageRules`, which also needs `withdrawal`; omit it and that comes back null. Accepts number, years, between 0 and 120.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 120,
              "x-unit": "years"
            }
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "Filing status in retirement, which sets the standard deduction and the brackets the withdrawal fills. Accepts one of single, married, hoh, mfs. 6 of the listed spellings are aliases: they are resolved to one of 4 canonical values (`x-canonical-values`) before the answer echoes them. Matching ignores case.",
            "schema": {
              "type": "string",
              "enum": [
                "single",
                "married",
                "hoh",
                "mfs",
                "married filing jointly",
                "mfj",
                "joint",
                "head of household",
                "married filing separately",
                "separate"
              ],
              "x-canonical-values": [
                "single",
                "married",
                "hoh",
                "mfs"
              ],
              "default": "single"
            }
          },
          {
            "name": "magi",
            "in": "query",
            "required": false,
            "description": "Today's modified adjusted gross income. Selects `eligibility` only — it changes no arithmetic above. Omit it and that comes back null. Accepts number, US dollars, between 0 and 100000000.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 100000000,
              "x-unit": "usd",
              "examples": [
                120000
              ]
            }
          },
          {
            "name": "age50Plus",
            "in": "query",
            "required": false,
            "description": "Whether the catch-up limit applies, which raises the cap from $7,500 to $8,600. This is your age **today**, in the contribution year — `age` above is your age at the withdrawal, decades later. Affects `limitEdge` and `eligibility`. Accepts boolean.",
            "schema": {
              "type": "boolean",
              "default": false
            }
          }
        ]
      },
      "post": {
        "tags": [
          "calculators"
        ],
        "summary": "Roth vs Traditional",
        "description": "Which account leaves you with more after tax, compared at the same cost to you today — plus the effective federal rate a Traditional withdrawal actually pays, which is neither the bracket everyone quotes nor the 20% a plan withholds.\n\n**The retirement rate people quote is a bracket, and a withdrawal is not taxed at a bracket.** A Traditional withdrawal is ordinary income filling the brackets from the bottom — the standard deduction first at 0%, then 10%, then 12%. On $60,000 withdrawn by a single filer with no other taxable income, the marginal bracket is **12%** and the effective federal rate is **8.37%**. Someone comparing \"22% now against 22% later\" and concluding it is a wash has the later figure wrong by more than the decision is worth. Send `withdrawal` and use the `effectiveRatePct` that comes back as `taxRetire`.\n\n**That rate is a floor, not a forecast, and this endpoint says so in the response.** It assumes the withdrawal is the only ordinary income that year. A pension, a spouse's wages, interest, and required distributions from any other pre-tax account all stack underneath it and push it into higher bands — the same $60,000 on top of $40,000 of other income pays **17.58%**, more than double. That is what `otherTaxableIncome` is for, and why `assumesOnlyIncome` is returned rather than assumed.\n\n**The percentage a plan withheld is not a tax rate at all, and it is usually too much.** A retirement plan distribution paid to you carries mandatory withholding of 20% — the IRS's words are \"even if you intend to roll it over later\" — so it is a deposit against a bill computed months later, identical for everyone and knowing nothing about the filer. On the $60,000 above, the plan sends $12,000 against $5,020 owed: $6,980 lent to the Treasury, earning nothing, until the return is filed. The `withholding` block measures that gap, and `breakEvenWithdrawal` says where it closes — about $239,133 withdrawn in one year by that filer, which is why over-withholding is the normal case rather than an edge case. An IRA is the contrast that proves it is a default and not a rate: 10%, and electable out — send `account=ira` for it. https://www.irs.gov/retirement-plans/plan-participant-employee/rollovers-of-retirement-plan-and-ira-distributions\n\n**The 10% before 59½ is charged on the distribution, not on the tax** — which makes it larger than the surcharge people picture. Send `age` and `ageRules` prices it: the same $60,000 taken at 50 owes $5,020 of income tax and $6,000 of additional tax — the penalty is the **bigger** of the two — for an all-in federal rate of 18.37% against the 8.37% the same withdrawal costs after 59½. Because it lands on the distribution it adds exactly its own 10 points to the rate, whatever the rate was; a withdrawal small enough to owe no income tax at all still owes it. **The 72(t) exceptions are not modelled** — disability, death, substantially equal periodic payments, qualified birth or adoption, certain medical expenses, a domestic-abuse distribution, separation from service at 55 or later for a workplace plan — so `penalty` is what applies if none of them fits, which is what `assumesNoException` says out loud. A rollover is not an early distribution: the additional tax reaches only the taxable part. https://www.irs.gov/taxtopics/tc558\n\n**RMDs are reported, not computed.** `ageRules` answers whether you have reached 73 and how many years are left, because the amount depends on a life-expectancy table and the prior 31 December balance that this endpoint does not ask for. Two timing notes that are this endpoint's own arithmetic applied to a deadline: the first RMD can be deferred to April 1 of the following year, which stacks two distributions into one calendar year and raises the effective rate on both, and a participant still working for the employer sponsoring the plan can generally delay plan RMDs until they retire unless they own 5% or more of the business. Roth IRAs have no RMDs while the owner is alive. https://www.irs.gov/retirement-plans/retirement-plan-and-ira-required-minimum-distributions-faqs\n\n**Social Security benefit taxation is not modelled**, and it is the largest thing missing. A Traditional withdrawal can raise the share of a benefit that becomes taxable, which is a second effect on top of the tax on the withdrawal itself. A retiree drawing Social Security should read `retirementRate` as an understatement.\n\n**At the same tax rate now and later the two accounts are mathematically identical**, and this endpoint returns a tie rather than a winner. `(1 − t)·(1 + r)ⁿ` and `(1 + r)ⁿ·(1 − t)` are the same number. Any calculator that shows the Roth winning at equal rates is comparing equal **contributions** rather than equal cost — which compares a larger sacrifice against a smaller one. See `contribution` above.\n\n**The cap is the one argument for the Roth that does not depend on predicting tax rates.** $7,500 into a Roth shelters $7,500 of after-tax money; the same nominal amount into a Traditional costs only $5,850 after tax at a 22% rate. Matching the Roth would take $9,615 of pre-tax money, which the cap does not allow. `limitEdge` reports that gap. The deferred tax is real and stays in your pocket, but it lands in a taxable account whose drag this endpoint does not model, so it is returned as `taxSavings` and not projected.\n\n**Federal only, and not a tax return.** No state income tax (see `/api/v1/state-tax`), no IRMAA, no Net Investment Income Tax, no credits, no age-65 additional standard deduction, and no RMD schedule. Figures are for tax year 2026: standard deduction and brackets from IRS Rev. Proc. 2025-32, limits from IRS Notice 2025-67.\n\n**The eligibility object is the Roth contribution limit only.** Whether a **Traditional** contribution is **deductible** phases out on different ranges that also depend on whether you or a spouse are covered by a workplace plan, and that is not modelled here — an undeducted Traditional contribution changes the comparison completely, so do not read a `full` band as advice about the other account. Roth phase-out ranges for 2026: $153,000–$168,000 single and head of household, $242,000–$252,000 married filing jointly, and $0–$10,000 filing separately, which never adjusts for inflation. https://www.irs.gov/newsroom/401k-limit-increases-to-24500-for-2026-ira-limit-increases-to-7500\n\nNothing here is advice about which account to choose. The comparison is arithmetic on the rates you supply, and the rate that matters most is the one this endpoint computes rather than asks you to guess.\n\nIdentical to the GET, with the parameters in a JSON object body.",
        "externalDocs": {
          "description": "The roth vs traditional calculator on farbetteroff.com",
          "url": "https://farbetteroff.com/calculators/roth-vs-traditional-calculator"
        },
        "responses": {
          "200": {
            "$ref": "#/components/responses/roth-vs-traditional-result"
          },
          "400": {
            "description": "A parameter is missing, mistyped, out of range or unknown — all of them are listed at once. A name that is one or two edits from a real parameter is answered with the one it probably meant; an ambiguous one is answered with the accepted list instead of a guess.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such endpoint. The response lists every valid slug.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "More than 60 requests in 60 seconds from one IP.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "A bug on our side. The inputs are never at fault for this one.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "operationId": "post-roth-vs-traditional",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "taxNow",
                  "taxRetire"
                ],
                "properties": {
                  "contribution": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 100000,
                    "default": 7500,
                    "x-unit": "usd",
                    "description": "Yearly contribution, measured as **what leaves your pocket after tax**. Defaults to the 2026 IRA limit. See the notes — this is not the same as the figure you type into a brokerage form. Accepts number, US dollars, between 0 and 100000."
                  },
                  "years": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 60,
                    "default": 30,
                    "x-unit": "years",
                    "examples": [
                      30
                    ],
                    "description": "Years of contributions and growth before you start withdrawing. Accepts integer, years, between 1 and 60."
                  },
                  "rate": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 20,
                    "default": 7,
                    "x-unit": "percent",
                    "description": "Annual return, the same for both accounts. Accepts number, a percentage, so 6.5 means 6.5%, between 0 and 20."
                  },
                  "taxNow": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 100,
                    "x-unit": "percent",
                    "examples": [
                      22
                    ],
                    "description": "Your marginal tax rate today — the bracket the contribution would otherwise be taxed in. Accepts number, a percentage, so 6.5 means 6.5%, between 0 and 100."
                  },
                  "taxRetire": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 100,
                    "x-unit": "percent",
                    "examples": [
                      22
                    ],
                    "description": "The rate you expect the Traditional withdrawal to pay. **Send the effective rate, not a bracket** — `withdrawal` below computes it for you. Accepts number, a percentage, so 6.5 means 6.5%, between 0 and 100."
                  },
                  "withdrawal": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 10000000,
                    "x-unit": "usd",
                    "examples": [
                      60000
                    ],
                    "description": "A year's planned withdrawal from the pre-tax account. Drives the whole `retirementRate` object, which is the point of this endpoint; omit it and that comes back null. Accepts number, US dollars, between 0 and 10000000."
                  },
                  "otherTaxableIncome": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 10000000,
                    "default": 0,
                    "x-unit": "usd",
                    "description": "Other ordinary taxable income in the same retirement year — a pension, a spouse's wages, another account's required distribution. The withdrawal stacks on top of it, so this raises the rate. Social Security is not modelled; see the notes. Accepts number, US dollars, between 0 and 10000000."
                  },
                  "withheldPct": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 100,
                    "x-unit": "percent",
                    "description": "What the payer actually withheld, if you have the statement in front of you. Omit it and `withholding` uses the default for `account` — 20% or 10% — and flags it as assumed. Withholding is a deposit, not a rate; it changes no arithmetic above. Accepts number, a percentage, so 6.5 means 6.5%, between 0 and 100."
                  },
                  "account": {
                    "type": "string",
                    "enum": [
                      "plan",
                      "ira",
                      "401k",
                      "401(k)",
                      "403b",
                      "457b",
                      "employer",
                      "workplace",
                      "traditional ira",
                      "rollover ira"
                    ],
                    "x-canonical-values": [
                      "plan",
                      "ira"
                    ],
                    "default": "plan",
                    "description": "Where the withdrawal is paid from, which sets the default withholding when `withheldPct` is omitted. A workplace plan withholds 20% and you cannot decline it; an IRA withholds 10% and you can elect out. Selects a default only — it changes no arithmetic. Accepts one of plan, ira. 8 of the listed spellings are aliases: they are resolved to one of 2 canonical values (`x-canonical-values`) before the answer echoes them. Matching ignores case."
                  },
                  "age": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 120,
                    "x-unit": "years",
                    "description": "Your age **when the withdrawal is taken**, which is a different age from `age50Plus` below — that one is about this year's contribution. Halves count, because the threshold is one: 59.4 is early and 59.5 is not. Selects `ageRules`, which also needs `withdrawal`; omit it and that comes back null. Accepts number, years, between 0 and 120."
                  },
                  "status": {
                    "type": "string",
                    "enum": [
                      "single",
                      "married",
                      "hoh",
                      "mfs",
                      "married filing jointly",
                      "mfj",
                      "joint",
                      "head of household",
                      "married filing separately",
                      "separate"
                    ],
                    "x-canonical-values": [
                      "single",
                      "married",
                      "hoh",
                      "mfs"
                    ],
                    "default": "single",
                    "description": "Filing status in retirement, which sets the standard deduction and the brackets the withdrawal fills. Accepts one of single, married, hoh, mfs. 6 of the listed spellings are aliases: they are resolved to one of 4 canonical values (`x-canonical-values`) before the answer echoes them. Matching ignores case."
                  },
                  "magi": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 100000000,
                    "x-unit": "usd",
                    "examples": [
                      120000
                    ],
                    "description": "Today's modified adjusted gross income. Selects `eligibility` only — it changes no arithmetic above. Omit it and that comes back null. Accepts number, US dollars, between 0 and 100000000."
                  },
                  "age50Plus": {
                    "type": "boolean",
                    "default": false,
                    "description": "Whether the catch-up limit applies, which raises the cap from $7,500 to $8,600. This is your age **today**, in the contribution year — `age` above is your age at the withdrawal, decades later. Affects `limitEdge` and `eligibility`. Accepts boolean."
                  }
                }
              }
            }
          }
        }
      }
    },
    "/refinance": {
      "get": {
        "tags": [
          "calculators"
        ],
        "summary": "Mortgage refinance break-even",
        "description": "The new payment, the monthly saving, how long it takes to earn the closing costs back, and whether resetting the term costs more interest anyway.\n\nA rate-and-term refinance: both loans are measured on the same balance, so this does not model cash-out, points bought at closing, or rolling the costs into the loan.\n\nThe interest figures cover each loan's own full remaining term, so they are not a like-for-like span when the terms differ. That is what makes `resetsTheTerm` visible instead of hidden.\n\nBreak-even ignores what the saved payment could earn if invested, and assumes you keep the loan to payoff.",
        "externalDocs": {
          "description": "The mortgage refinance break-even calculator on farbetteroff.com",
          "url": "https://farbetteroff.com/calculators/refinance-calculator"
        },
        "responses": {
          "200": {
            "$ref": "#/components/responses/refinance-result"
          },
          "400": {
            "description": "A parameter is missing, mistyped, out of range or unknown — all of them are listed at once. A name that is one or two edits from a real parameter is answered with the one it probably meant; an ambiguous one is answered with the accepted list instead of a guess.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such endpoint. The response lists every valid slug.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "More than 60 requests in 60 seconds from one IP.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "A bug on our side. The inputs are never at fault for this one.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "operationId": "get-refinance",
        "parameters": [
          {
            "name": "balance",
            "in": "query",
            "required": true,
            "description": "What is still owed on the current mortgage. Accepts number, US dollars, between 0 and 10000000000.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 10000000000,
              "x-unit": "usd",
              "examples": [
                320000
              ]
            }
          },
          {
            "name": "currentRate",
            "in": "query",
            "required": true,
            "description": "The rate on the loan you have today. Accepts number, a percentage, so 6.5 means 6.5%, between 0 and 100.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 100,
              "x-unit": "percent",
              "examples": [
                7.25
              ]
            }
          },
          {
            "name": "yearsLeft",
            "in": "query",
            "required": true,
            "description": "Years remaining on the current loan. Accepts number, years, between 0 and 50.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 50,
              "x-unit": "years",
              "examples": [
                27
              ]
            }
          },
          {
            "name": "newRate",
            "in": "query",
            "required": true,
            "description": "The rate you are being offered. Accepts number, a percentage, so 6.5 means 6.5%, between 0 and 100.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 100,
              "x-unit": "percent",
              "examples": [
                6
              ]
            }
          },
          {
            "name": "newTermYears",
            "in": "query",
            "required": false,
            "description": "Term of the new loan, in years. Accepts integer, years, between 1 and 50.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 50,
              "default": 30,
              "x-unit": "years",
              "examples": [
                30
              ]
            }
          },
          {
            "name": "closingCosts",
            "in": "query",
            "required": false,
            "description": "Closing costs on the new loan, paid up front. Typically 2%–5% of the loan. Accepts number, US dollars, between 0 and 100000000.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 100000000,
              "default": 0,
              "x-unit": "usd",
              "examples": [
                6000
              ]
            }
          }
        ]
      },
      "post": {
        "tags": [
          "calculators"
        ],
        "summary": "Mortgage refinance break-even",
        "description": "The new payment, the monthly saving, how long it takes to earn the closing costs back, and whether resetting the term costs more interest anyway.\n\nA rate-and-term refinance: both loans are measured on the same balance, so this does not model cash-out, points bought at closing, or rolling the costs into the loan.\n\nThe interest figures cover each loan's own full remaining term, so they are not a like-for-like span when the terms differ. That is what makes `resetsTheTerm` visible instead of hidden.\n\nBreak-even ignores what the saved payment could earn if invested, and assumes you keep the loan to payoff.\n\nIdentical to the GET, with the parameters in a JSON object body.",
        "externalDocs": {
          "description": "The mortgage refinance break-even calculator on farbetteroff.com",
          "url": "https://farbetteroff.com/calculators/refinance-calculator"
        },
        "responses": {
          "200": {
            "$ref": "#/components/responses/refinance-result"
          },
          "400": {
            "description": "A parameter is missing, mistyped, out of range or unknown — all of them are listed at once. A name that is one or two edits from a real parameter is answered with the one it probably meant; an ambiguous one is answered with the accepted list instead of a guess.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such endpoint. The response lists every valid slug.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "More than 60 requests in 60 seconds from one IP.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "A bug on our side. The inputs are never at fault for this one.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "operationId": "post-refinance",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "balance",
                  "currentRate",
                  "yearsLeft",
                  "newRate"
                ],
                "properties": {
                  "balance": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 10000000000,
                    "x-unit": "usd",
                    "examples": [
                      320000
                    ],
                    "description": "What is still owed on the current mortgage. Accepts number, US dollars, between 0 and 10000000000."
                  },
                  "currentRate": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 100,
                    "x-unit": "percent",
                    "examples": [
                      7.25
                    ],
                    "description": "The rate on the loan you have today. Accepts number, a percentage, so 6.5 means 6.5%, between 0 and 100."
                  },
                  "yearsLeft": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 50,
                    "x-unit": "years",
                    "examples": [
                      27
                    ],
                    "description": "Years remaining on the current loan. Accepts number, years, between 0 and 50."
                  },
                  "newRate": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 100,
                    "x-unit": "percent",
                    "examples": [
                      6
                    ],
                    "description": "The rate you are being offered. Accepts number, a percentage, so 6.5 means 6.5%, between 0 and 100."
                  },
                  "newTermYears": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 50,
                    "default": 30,
                    "x-unit": "years",
                    "examples": [
                      30
                    ],
                    "description": "Term of the new loan, in years. Accepts integer, years, between 1 and 50."
                  },
                  "closingCosts": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 100000000,
                    "default": 0,
                    "x-unit": "usd",
                    "examples": [
                      6000
                    ],
                    "description": "Closing costs on the new loan, paid up front. Typically 2%–5% of the loan. Accepts number, US dollars, between 0 and 100000000."
                  }
                }
              }
            }
          }
        }
      }
    },
    "/rent-vs-buy": {
      "get": {
        "tags": [
          "calculators"
        ],
        "summary": "Rent vs buy",
        "description": "Net worth year by year down both paths, counting what the renter earns investing the down payment, and the year buying pulls ahead.\n\nBoth people start with the same cash. The buyer spends it on the down payment and buying costs; the renter invests it at `investReturn`, and each month the cheaper path invests the difference. That opportunity cost is what most rent-vs-buy comparisons leave out.\n\nThe buyer's net worth is equity after selling costs, so it is what they would walk away with — which is why buying normally starts well behind.\n\nAnnual rates compound monthly ((1 + r)^(1/12) − 1), so 7% means 7% a year, not 7%/12 a month.\n\n**PMI is charged below 20% down, and it is the cost that moves this answer most.** The premium falls entirely in the early years, which is where the break-even lives: on `years=7&homePrice=400000&monthlyRent=2400&downPaymentPercent=5&rate=6.5` it is $158.33 a month for 135 months — $13,300 inside a 7-year stay — and it moves `breakEvenYear` from 5 to 6 and `difference` from $26,559 to $9,595, which is more than the premium itself because the renter invests every month owning costs more. It stops on the CFPB's automatic termination date — the month the scheduled balance reaches 78% of the price, with the loan's midpoint as a backstop — the same date /api/v1/pmi returns for the same loan. `pmiRatePct=0` turns it off.\n\nNot modelled: the mortgage interest deduction, and capital-gains treatment on the sale. Each needs assumptions about one person's taxes, and each moves the answer.",
        "externalDocs": {
          "description": "The rent vs buy calculator on farbetteroff.com",
          "url": "https://farbetteroff.com/calculators/rent-vs-buy-calculator"
        },
        "responses": {
          "200": {
            "$ref": "#/components/responses/rent-vs-buy-result"
          },
          "400": {
            "description": "A parameter is missing, mistyped, out of range or unknown — all of them are listed at once. A name that is one or two edits from a real parameter is answered with the one it probably meant; an ambiguous one is answered with the accepted list instead of a guess.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such endpoint. The response lists every valid slug.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "More than 60 requests in 60 seconds from one IP.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "A bug on our side. The inputs are never at fault for this one.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "operationId": "get-rent-vs-buy",
        "parameters": [
          {
            "name": "years",
            "in": "query",
            "required": true,
            "description": "How long you would stay before selling or moving out. Accepts integer, years, between 1 and 50.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 50,
              "x-unit": "years",
              "examples": [
                7
              ]
            }
          },
          {
            "name": "homePrice",
            "in": "query",
            "required": true,
            "description": "Purchase price. Accepts number, US dollars, between 0 and 10000000000.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 10000000000,
              "x-unit": "usd",
              "examples": [
                400000
              ]
            }
          },
          {
            "name": "monthlyRent",
            "in": "query",
            "required": true,
            "description": "Rent for a comparable place, per month, today. Accepts number, US dollars, between 0 and 1000000.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 1000000,
              "x-unit": "usd",
              "examples": [
                2400
              ]
            }
          },
          {
            "name": "downPaymentPercent",
            "in": "query",
            "required": false,
            "description": "Down payment as a percentage of the price. Accepts number, a percentage, so 6.5 means 6.5%, between 0 and 100.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 100,
              "default": 20,
              "x-unit": "percent",
              "examples": [
                20
              ]
            }
          },
          {
            "name": "rate",
            "in": "query",
            "required": false,
            "description": "Mortgage rate (APR). Accepts number, a percentage, so 6.5 means 6.5%, between 0 and 100.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 100,
              "default": 6.5,
              "x-unit": "percent",
              "examples": [
                6.5
              ]
            }
          },
          {
            "name": "termYears",
            "in": "query",
            "required": false,
            "description": "Length of the mortgage. Accepts integer, years, between 1 and 50.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 50,
              "default": 30,
              "x-unit": "years"
            }
          },
          {
            "name": "homeGrowth",
            "in": "query",
            "required": false,
            "description": "Annual home appreciation. May be negative. Accepts number, a percentage, so 6.5 means 6.5%, between -20 and 30.",
            "schema": {
              "type": "number",
              "minimum": -20,
              "maximum": 30,
              "default": 4,
              "x-unit": "percent"
            }
          },
          {
            "name": "rentGrowth",
            "in": "query",
            "required": false,
            "description": "Annual rent increase, applied on each anniversary. Accepts number, a percentage, so 6.5 means 6.5%, between -20 and 30.",
            "schema": {
              "type": "number",
              "minimum": -20,
              "maximum": 30,
              "default": 3,
              "x-unit": "percent"
            }
          },
          {
            "name": "investReturn",
            "in": "query",
            "required": false,
            "description": "What invested cash earns annually — the down payment's opportunity cost. Accepts number, a percentage, so 6.5 means 6.5%, between 0 and 30.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 30,
              "default": 7,
              "x-unit": "percent"
            }
          },
          {
            "name": "propertyTaxPercent",
            "in": "query",
            "required": false,
            "description": "Property tax per year, as a percentage of the home's current value. Accepts number, a percentage, so 6.5 means 6.5%, between 0 and 10.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 10,
              "default": 1.1,
              "x-unit": "percent"
            }
          },
          {
            "name": "maintenancePercent",
            "in": "query",
            "required": false,
            "description": "Maintenance per year, as a percentage of the home's current value. Accepts number, a percentage, so 6.5 means 6.5%, between 0 and 10.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 10,
              "default": 1,
              "x-unit": "percent"
            }
          },
          {
            "name": "insuranceAnnual",
            "in": "query",
            "required": false,
            "description": "Homeowner's insurance per year. Accepts number, US dollars, between 0 and 1000000.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 1000000,
              "default": 1800,
              "x-unit": "usd"
            }
          },
          {
            "name": "hoaMonthly",
            "in": "query",
            "required": false,
            "description": "HOA dues per month. Accepts number, US dollars, between 0 and 100000.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 100000,
              "default": 0,
              "x-unit": "usd"
            }
          },
          {
            "name": "buyClosingPercent",
            "in": "query",
            "required": false,
            "description": "Buying costs paid up front, as a percentage of the price. Accepts number, a percentage, so 6.5 means 6.5%, between 0 and 20.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 20,
              "default": 2,
              "x-unit": "percent"
            }
          },
          {
            "name": "sellClosingPercent",
            "in": "query",
            "required": false,
            "description": "Selling costs, as a percentage of the sale price — agent commission and closing. Accepts number, a percentage, so 6.5 means 6.5%, between 0 and 20.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 20,
              "default": 6,
              "x-unit": "percent"
            }
          },
          {
            "name": "pmiRatePct",
            "in": "query",
            "required": false,
            "description": "Annual private mortgage insurance as a percent of the loan, charged only when the down payment leaves the loan above 80% of the price. Defaults to 0.5%, mid-range for the $30–$70 a month per $100,000 borrowed Freddie Mac describes. Send `pmiRatePct=0` for a comparison with no mortgage insurance in it — the right call for a VA loan. Accepts number, a percentage, so 6.5 means 6.5%, between 0 and 5.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 5,
              "default": 0.5,
              "x-unit": "percent"
            }
          }
        ]
      },
      "post": {
        "tags": [
          "calculators"
        ],
        "summary": "Rent vs buy",
        "description": "Net worth year by year down both paths, counting what the renter earns investing the down payment, and the year buying pulls ahead.\n\nBoth people start with the same cash. The buyer spends it on the down payment and buying costs; the renter invests it at `investReturn`, and each month the cheaper path invests the difference. That opportunity cost is what most rent-vs-buy comparisons leave out.\n\nThe buyer's net worth is equity after selling costs, so it is what they would walk away with — which is why buying normally starts well behind.\n\nAnnual rates compound monthly ((1 + r)^(1/12) − 1), so 7% means 7% a year, not 7%/12 a month.\n\n**PMI is charged below 20% down, and it is the cost that moves this answer most.** The premium falls entirely in the early years, which is where the break-even lives: on `years=7&homePrice=400000&monthlyRent=2400&downPaymentPercent=5&rate=6.5` it is $158.33 a month for 135 months — $13,300 inside a 7-year stay — and it moves `breakEvenYear` from 5 to 6 and `difference` from $26,559 to $9,595, which is more than the premium itself because the renter invests every month owning costs more. It stops on the CFPB's automatic termination date — the month the scheduled balance reaches 78% of the price, with the loan's midpoint as a backstop — the same date /api/v1/pmi returns for the same loan. `pmiRatePct=0` turns it off.\n\nNot modelled: the mortgage interest deduction, and capital-gains treatment on the sale. Each needs assumptions about one person's taxes, and each moves the answer.\n\nIdentical to the GET, with the parameters in a JSON object body.",
        "externalDocs": {
          "description": "The rent vs buy calculator on farbetteroff.com",
          "url": "https://farbetteroff.com/calculators/rent-vs-buy-calculator"
        },
        "responses": {
          "200": {
            "$ref": "#/components/responses/rent-vs-buy-result"
          },
          "400": {
            "description": "A parameter is missing, mistyped, out of range or unknown — all of them are listed at once. A name that is one or two edits from a real parameter is answered with the one it probably meant; an ambiguous one is answered with the accepted list instead of a guess.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such endpoint. The response lists every valid slug.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "More than 60 requests in 60 seconds from one IP.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "A bug on our side. The inputs are never at fault for this one.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "operationId": "post-rent-vs-buy",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "years",
                  "homePrice",
                  "monthlyRent"
                ],
                "properties": {
                  "years": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 50,
                    "x-unit": "years",
                    "examples": [
                      7
                    ],
                    "description": "How long you would stay before selling or moving out. Accepts integer, years, between 1 and 50."
                  },
                  "homePrice": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 10000000000,
                    "x-unit": "usd",
                    "examples": [
                      400000
                    ],
                    "description": "Purchase price. Accepts number, US dollars, between 0 and 10000000000."
                  },
                  "monthlyRent": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 1000000,
                    "x-unit": "usd",
                    "examples": [
                      2400
                    ],
                    "description": "Rent for a comparable place, per month, today. Accepts number, US dollars, between 0 and 1000000."
                  },
                  "downPaymentPercent": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 100,
                    "default": 20,
                    "x-unit": "percent",
                    "examples": [
                      20
                    ],
                    "description": "Down payment as a percentage of the price. Accepts number, a percentage, so 6.5 means 6.5%, between 0 and 100."
                  },
                  "rate": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 100,
                    "default": 6.5,
                    "x-unit": "percent",
                    "examples": [
                      6.5
                    ],
                    "description": "Mortgage rate (APR). Accepts number, a percentage, so 6.5 means 6.5%, between 0 and 100."
                  },
                  "termYears": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 50,
                    "default": 30,
                    "x-unit": "years",
                    "description": "Length of the mortgage. Accepts integer, years, between 1 and 50."
                  },
                  "homeGrowth": {
                    "type": "number",
                    "minimum": -20,
                    "maximum": 30,
                    "default": 4,
                    "x-unit": "percent",
                    "description": "Annual home appreciation. May be negative. Accepts number, a percentage, so 6.5 means 6.5%, between -20 and 30."
                  },
                  "rentGrowth": {
                    "type": "number",
                    "minimum": -20,
                    "maximum": 30,
                    "default": 3,
                    "x-unit": "percent",
                    "description": "Annual rent increase, applied on each anniversary. Accepts number, a percentage, so 6.5 means 6.5%, between -20 and 30."
                  },
                  "investReturn": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 30,
                    "default": 7,
                    "x-unit": "percent",
                    "description": "What invested cash earns annually — the down payment's opportunity cost. Accepts number, a percentage, so 6.5 means 6.5%, between 0 and 30."
                  },
                  "propertyTaxPercent": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 10,
                    "default": 1.1,
                    "x-unit": "percent",
                    "description": "Property tax per year, as a percentage of the home's current value. Accepts number, a percentage, so 6.5 means 6.5%, between 0 and 10."
                  },
                  "maintenancePercent": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 10,
                    "default": 1,
                    "x-unit": "percent",
                    "description": "Maintenance per year, as a percentage of the home's current value. Accepts number, a percentage, so 6.5 means 6.5%, between 0 and 10."
                  },
                  "insuranceAnnual": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 1000000,
                    "default": 1800,
                    "x-unit": "usd",
                    "description": "Homeowner's insurance per year. Accepts number, US dollars, between 0 and 1000000."
                  },
                  "hoaMonthly": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 100000,
                    "default": 0,
                    "x-unit": "usd",
                    "description": "HOA dues per month. Accepts number, US dollars, between 0 and 100000."
                  },
                  "buyClosingPercent": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 20,
                    "default": 2,
                    "x-unit": "percent",
                    "description": "Buying costs paid up front, as a percentage of the price. Accepts number, a percentage, so 6.5 means 6.5%, between 0 and 20."
                  },
                  "sellClosingPercent": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 20,
                    "default": 6,
                    "x-unit": "percent",
                    "description": "Selling costs, as a percentage of the sale price — agent commission and closing. Accepts number, a percentage, so 6.5 means 6.5%, between 0 and 20."
                  },
                  "pmiRatePct": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 5,
                    "default": 0.5,
                    "x-unit": "percent",
                    "description": "Annual private mortgage insurance as a percent of the loan, charged only when the down payment leaves the loan above 80% of the price. Defaults to 0.5%, mid-range for the $30–$70 a month per $100,000 borrowed Freddie Mac describes. Send `pmiRatePct=0` for a comparison with no mortgage insurance in it — the right call for a VA loan. Accepts number, a percentage, so 6.5 means 6.5%, between 0 and 5."
                  }
                }
              }
            }
          }
        }
      }
    },
    "/rule-of-72": {
      "get": {
        "tags": [
          "calculators"
        ],
        "summary": "Rule of 72",
        "description": "Roughly how long money takes to double at a given rate, and the exact answer beside it.\n\nThe Rule of 72 is a mental-arithmetic approximation. It is accurate to within a few months over the 5%–12% range and drifts outside it — `exactYears` is the number to use when precision matters.",
        "externalDocs": {
          "description": "The rule of 72 calculator on farbetteroff.com",
          "url": "https://farbetteroff.com/calculators/compound-interest-calculator"
        },
        "responses": {
          "200": {
            "$ref": "#/components/responses/rule-of-72-result"
          },
          "400": {
            "description": "A parameter is missing, mistyped, out of range or unknown — all of them are listed at once. A name that is one or two edits from a real parameter is answered with the one it probably meant; an ambiguous one is answered with the accepted list instead of a guess.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such endpoint. The response lists every valid slug.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "More than 60 requests in 60 seconds from one IP.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "A bug on our side. The inputs are never at fault for this one.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "operationId": "get-rule-of-72",
        "parameters": [
          {
            "name": "rate",
            "in": "query",
            "required": true,
            "description": "Annual rate of return. Accepts number, a percentage, so 6.5 means 6.5%, between 0 and 100.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 100,
              "x-unit": "percent",
              "examples": [
                8
              ]
            }
          }
        ]
      },
      "post": {
        "tags": [
          "calculators"
        ],
        "summary": "Rule of 72",
        "description": "Roughly how long money takes to double at a given rate, and the exact answer beside it.\n\nThe Rule of 72 is a mental-arithmetic approximation. It is accurate to within a few months over the 5%–12% range and drifts outside it — `exactYears` is the number to use when precision matters.\n\nIdentical to the GET, with the parameters in a JSON object body.",
        "externalDocs": {
          "description": "The rule of 72 calculator on farbetteroff.com",
          "url": "https://farbetteroff.com/calculators/compound-interest-calculator"
        },
        "responses": {
          "200": {
            "$ref": "#/components/responses/rule-of-72-result"
          },
          "400": {
            "description": "A parameter is missing, mistyped, out of range or unknown — all of them are listed at once. A name that is one or two edits from a real parameter is answered with the one it probably meant; an ambiguous one is answered with the accepted list instead of a guess.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such endpoint. The response lists every valid slug.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "More than 60 requests in 60 seconds from one IP.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "A bug on our side. The inputs are never at fault for this one.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "operationId": "post-rule-of-72",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "rate"
                ],
                "properties": {
                  "rate": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 100,
                    "x-unit": "percent",
                    "examples": [
                      8
                    ],
                    "description": "Annual rate of return. Accepts number, a percentage, so 6.5 means 6.5%, between 0 and 100."
                  }
                }
              }
            }
          }
        }
      }
    },
    "/rates": {
      "get": {
        "tags": [
          "datasets"
        ],
        "summary": "Today's US consumer interest rates",
        "description": "The nine national average rates the site runs on, each with its own reading date: 30- and 15-year fixed mortgages, the Fed funds target, the savings and 12-month CD averages, the 10-year Treasury, and the Federal Reserve's credit card, new-car and personal-loan averages. Every row names the body that published it — Freddie Mac, the FDIC or the Federal Reserve Board — rather than the aggregator it was read through. Takes no parameters; refetched every six hours.\n\nTakes no parameters. Read from nine FRED series every six hours and served unchanged, rounded to the two decimals each body publishes. Nothing here is computed except the change against the previous reading and against a year earlier, which are subtractions of readings the same answer carries. Each row states its own asOf, its publisher and FRED's declared frequency for the series, because the nine do not move together: the mortgage averages print weekly, the Fed funds target and the Treasury yield daily, and the three consumer-credit averages only four times a year.\n\nThe same figure with every step of its arithmetic shown is at https://farbetteroff.com/rates.",
        "operationId": "get-rates",
        "parameters": [],
        "externalDocs": {
          "description": "The same figure, with the arithmetic in full",
          "url": "https://farbetteroff.com/rates"
        },
        "responses": {
          "200": {
            "$ref": "#/components/responses/rates-data"
          },
          "429": {
            "description": "More than 60 requests in 60 seconds from one IP.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/income-needed-to-buy-a-house": {
      "get": {
        "tags": [
          "datasets"
        ],
        "summary": "Income needed to buy the median new home in the United States",
        "description": "The gross household income the 28% front-end ratio asks for on the median-priced new home at the current 30-year fixed rate, with the payment broken out, the same answer on 10% down, and the price the median household income actually reaches. Takes no parameters; rebuilt every six hours from three public series.\n\nTakes no parameters. Derived from MSPUS, MORTGAGE30US and MEHOINUSA646N on FRED, rebuilt every six hours. Every reading comes back in `sources`, so the answer can be checked rather than trusted.\n\nThe same figure with every step of its arithmetic shown is at https://farbetteroff.com/income-needed-to-buy-a-house.",
        "operationId": "get-income-needed-to-buy-a-house",
        "parameters": [],
        "externalDocs": {
          "description": "The same figure, with the arithmetic in full",
          "url": "https://farbetteroff.com/income-needed-to-buy-a-house"
        },
        "responses": {
          "200": {
            "$ref": "#/components/responses/income-needed-to-buy-a-house-data"
          },
          "429": {
            "description": "More than 60 requests in 60 seconds from one IP.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/state-income-tax-rates": {
      "get": {
        "tags": [
          "datasets"
        ],
        "summary": "State income tax on wages, 2026",
        "description": "What each of the 50 states and the District of Columbia charges on a paycheck: the 9 that levy no individual income tax on wages, the 16 with one statutory rate, the 4 whose published bracket schedules are modelled in full, and the 22 where the row is an explicit refusal rather than a number. Every verified row carries its revenue department's own page, its standard deduction, its zero band or surtax where it has one, and a top marginal rate. Takes no parameters.\n\nTakes no parameters. Read off each state's own revenue department, legislature or code and served unchanged, with that page cited in the row rather than in a footnote. Nothing is computed except topMarginalRatePercent, which is the same function the calculator answers with, asked at an income past every threshold in the state's schedule. A state whose bracket table has not been read off its own forms is a row with taxability \"unmodeled\" and no rate — never a zero, and never an estimate.\n\nThe same figure with every step of its arithmetic shown is at https://farbetteroff.com/state-income-tax-rates.",
        "operationId": "get-state-income-tax-rates",
        "parameters": [],
        "externalDocs": {
          "description": "The same figure, with the arithmetic in full",
          "url": "https://farbetteroff.com/state-income-tax-rates"
        },
        "responses": {
          "200": {
            "$ref": "#/components/responses/state-income-tax-rates-data"
          },
          "429": {
            "description": "More than 60 requests in 60 seconds from one IP.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/money-numbers": {
      "get": {
        "tags": [
          "datasets"
        ],
        "summary": "The 2026 money numbers",
        "description": "The 2026 US contribution limits, deductions and rates in one call: 401(k), IRA and HSA limits, the standard deduction and the senior deduction, the gift exclusion, the Social Security COLA, wage base and full retirement age, and I bond and FDIC figures. Every figure carries the notice, revenue procedure or section of the US Code that set it. Takes no parameters.\n\nTakes no parameters. Transcribed from the documents that set each figure — IRS notices and revenue procedures, a Federal Register notice and the US Code — and served with the document beside the number. Nothing is computed except the projected 2027 COLA, which is flagged and states its own arithmetic. These move when an agency publishes, which for most of them is once a year in the autumn.\n\nThe same figure with every step of its arithmetic shown is at https://farbetteroff.com/money-numbers.",
        "operationId": "get-money-numbers",
        "parameters": [],
        "externalDocs": {
          "description": "The same figure, with the arithmetic in full",
          "url": "https://farbetteroff.com/money-numbers"
        },
        "responses": {
          "200": {
            "$ref": "#/components/responses/money-numbers-data"
          },
          "429": {
            "description": "More than 60 requests in 60 seconds from one IP.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "Error": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "object",
            "required": [
              "code",
              "message"
            ],
            "properties": {
              "code": {
                "type": "string",
                "enum": [
                  "unknown_endpoint",
                  "invalid_params",
                  "invalid_json",
                  "rate_limited",
                  "internal_error"
                ]
              },
              "message": {
                "type": "string",
                "description": "A sentence a human can act on."
              },
              "issues": {
                "type": "array",
                "description": "Present on invalid_params: every bad parameter, not just the first.",
                "items": {
                  "type": "object",
                  "required": [
                    "param",
                    "message"
                  ],
                  "properties": {
                    "param": {
                      "type": "string",
                      "description": "The name you sent, exactly as you sent it."
                    },
                    "message": {
                      "type": "string"
                    },
                    "didYouMean": {
                      "type": "string",
                      "description": "Present when exactly one spelling is the probable intent: a declared parameter, when the name you sent is not one, or an accepted enum value, when the name is right and the value is not (status=marrried reads as married, state=Ohioo as OH). Absent rather than guessed when several are equally close; those appear in candidates instead, so at most one of the two keys is ever present. Absent on a value that is out of range or the wrong type rather than mistyped."
                    },
                    "candidates": {
                      "type": "array",
                      "description": "Present when two or more spellings are equally close and naming one would be a coin flip — apr on /debt-snowball is one edit from all six of apr1 … apr6, which are the six per-debt slots, and state=NI is one edit from thirteen real USPS codes. Pick the one you meant.",
                      "items": {
                        "type": "string"
                      }
                    }
                  }
                }
              },
              "didYouMean": {
                "type": "string",
                "description": "Present on unknown_endpoint when exactly one endpoint is the probable intent — a case difference, a near miss, or the calculator page the slug belongs to. Absent rather than guessed when two endpoints are equally good readings; those appear in candidates instead, so at most one of the two keys is ever present."
              },
              "candidates": {
                "type": "array",
                "description": "Present on unknown_endpoint when two or more endpoints are equally good readings and naming one would be a coin flip — typically a calculator page answered by more than one endpoint, as take-home-paycheck-calculator is by both paycheck and state-tax. Retry against whichever answers your question.",
                "items": {
                  "type": "string"
                }
              },
              "available": {
                "type": "array",
                "description": "Present on unknown_endpoint: every valid endpoint slug.",
                "items": {
                  "type": "string"
                }
              },
              "docs": {
                "type": "string",
                "format": "uri",
                "description": "Documentation for the endpoint that failed, anchored to its own section on farbetteroff.com/api — the parameter table, the bounds and a live console to retry in. On unknown_endpoint, where there is no endpoint to point at, this is the index instead."
              }
            }
          }
        }
      }
    },
    "responses": {
      "pay-result": {
        "description": "The pay, converted answer.",
        "headers": {
          "Cache-Control": {
            "description": "Long-lived: the same URL is the same answer forever.",
            "schema": {
              "type": "string"
            }
          },
          "X-RateLimit-Remaining": {
            "description": "Requests left in the current window.",
            "schema": {
              "type": "integer"
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "properties": {
                "endpoint": {
                  "type": "string",
                  "const": "pay"
                },
                "inputs": {
                  "type": "object",
                  "description": "The parsed inputs the answer was computed from: your request with defaults filled in, money parsed out of strings like \"$400,000\", and enum spellings canonicalised.",
                  "properties": {
                    "amount": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 1000000000,
                      "x-unit": "usd",
                      "description": "The pay figure you have."
                    },
                    "per": {
                      "type": "string",
                      "enum": [
                        "hour",
                        "day",
                        "week",
                        "biweek",
                        "semimonth",
                        "month",
                        "year"
                      ],
                      "description": "The period that amount is for. `biweek` is every two weeks (26 paychecks a year), `semimonth` twice a month (24)."
                    },
                    "hoursPerWeek": {
                      "type": "number",
                      "minimum": 0.5,
                      "maximum": 168,
                      "x-unit": "count",
                      "description": "Hours worked in a week. Always echoed; 40 when not sent."
                    },
                    "weeksPerYear": {
                      "type": "number",
                      "minimum": 1,
                      "maximum": 52,
                      "x-unit": "count",
                      "description": "Weeks **paid** in a year. Drop it for unpaid time off: two weeks off is 50. Always echoed; 52 when not sent."
                    },
                    "daysPerWeek": {
                      "type": "number",
                      "minimum": 1,
                      "maximum": 7,
                      "x-unit": "count",
                      "description": "Days worked in a week, which is what the daily figure divides by. Always echoed; 5 when not sent."
                    },
                    "overtimeAfterHours": {
                      "type": "number",
                      "minimum": 1,
                      "maximum": 168,
                      "x-unit": "count",
                      "description": "Hours in the week after which the premium starts. 40 is the federal floor; a contract or a state rule can start it sooner. Always echoed; 40 when not sent."
                    },
                    "overtimeMultiplier": {
                      "type": "number",
                      "minimum": 1,
                      "maximum": 3,
                      "description": "What an hour past the threshold pays, as a multiple of the base rate. Send `1` for a schedule with no premium — a salaried exempt job — and every figure is straight time again. Always echoed; 1.5 when not sent."
                    }
                  },
                  "required": [
                    "amount",
                    "per",
                    "hoursPerWeek",
                    "weeksPerYear",
                    "daysPerWeek",
                    "overtimeAfterHours",
                    "overtimeMultiplier"
                  ]
                },
                "result": {
                  "type": "object",
                  "description": "The answer.",
                  "properties": {
                    "hourly": {
                      "description": "Pay for one hour worked, averaged over the week: above the threshold this is higher than `baseHourly`, because some of those hours pay the premium."
                    },
                    "daily": {
                      "description": "Pay for one worked day."
                    },
                    "weekly": {
                      "description": "Pay for one paid week."
                    },
                    "biweekly": {
                      "description": "One paycheck when payroll runs every two weeks (26 a year)."
                    },
                    "semiMonthly": {
                      "description": "One paycheck when payroll runs twice a month (24 a year)."
                    },
                    "monthly": {
                      "description": "One month's pay — a twelfth of the year, not four weeks."
                    },
                    "annual": {
                      "description": "The yearly total."
                    },
                    "hoursPerYear": {
                      "description": "Paid hours in the year the hourly figure divides by."
                    },
                    "baseHourly": {
                      "description": "The straight-time rate behind the answer — what one of the first `overtimeAfterHours` hours pays. Sent an hourly `amount`, it is that amount; sent a salary, it is what an hourly job would have to pay to reach it once the premium is counted, which is less than the average hour."
                    },
                    "overtimeHours": {
                      "description": "Hours a week above the threshold. Zero on a schedule that never reaches it."
                    },
                    "overtimeRate": {
                      "description": "What an hour past the threshold pays: `baseHourly × overtimeMultiplier`. Quoted even on a week that never reaches it, because it is what the next hour would earn."
                    },
                    "overtimePremiumAnnual": {
                      "description": "What the premium alone adds over the year, against the same hours at straight time. Zero when there is no premium."
                    }
                  }
                },
                "notes": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                },
                "meta": {
                  "type": "object",
                  "description": "Provenance: the source site, the matching calculator page, the docs and the disclaimer.",
                  "properties": {
                    "source": {
                      "type": "string"
                    },
                    "calculator": {
                      "type": "string"
                    },
                    "docs": {
                      "type": "string"
                    },
                    "disclaimer": {
                      "type": "string"
                    }
                  }
                }
              },
              "required": [
                "endpoint",
                "inputs",
                "result",
                "meta"
              ]
            }
          }
        }
      },
      "paycheck-result": {
        "description": "The take-home pay (federal) answer.",
        "headers": {
          "Cache-Control": {
            "description": "Long-lived: the same URL is the same answer forever.",
            "schema": {
              "type": "string"
            }
          },
          "X-RateLimit-Remaining": {
            "description": "Requests left in the current window.",
            "schema": {
              "type": "integer"
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "properties": {
                "endpoint": {
                  "type": "string",
                  "const": "paycheck"
                },
                "inputs": {
                  "type": "object",
                  "description": "The parsed inputs the answer was computed from: your request with defaults filled in, money parsed out of strings like \"$400,000\", and enum spellings canonicalised.",
                  "properties": {
                    "salary": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 1000000000,
                      "x-unit": "usd",
                      "description": "Gross annual salary."
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "single",
                        "married",
                        "hoh",
                        "mfs"
                      ],
                      "description": "Filing status: single, married (filing jointly), hoh (head of household) or mfs (married filing separately). Always echoed; \"single\" when not sent."
                    },
                    "contribPercent": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 100,
                      "x-unit": "percent",
                      "description": "401(k)/403(b) contribution as a percent of salary. See contribType for which side of the tax line it falls on. Always echoed; 0 when not sent."
                    },
                    "contribType": {
                      "type": "string",
                      "enum": [
                        "traditional",
                        "roth"
                      ],
                      "description": "traditional: an elective deferral, exempt from income tax but not from FICA. roth: a designated Roth contribution, which the IRS includes in gross income when it is earned, so it reduces no tax base at all and only takeHome moves. Always echoed; \"traditional\" when not sent."
                    },
                    "catchUp": {
                      "type": "string",
                      "enum": [
                        "none",
                        "age50",
                        "age60to63"
                      ],
                      "description": "Which catch-up raises the § 402(g) deferral cap, by the age reached during the year: none caps the contribution at $24,500, age50 at $32,500, age60to63 at $35,750 (IRS Notice 2025-67). contribPercent is capped at whichever applies, so contribution can be less than the percent asked for — see contributionLimited. Always echoed; \"none\" when not sent."
                    },
                    "payPeriods": {
                      "type": "integer",
                      "minimum": 1,
                      "maximum": 365,
                      "x-unit": "count",
                      "description": "Paychecks per year: 52 weekly, 26 every two weeks, 24 twice a month, 12 monthly. Always echoed; 26 when not sent."
                    }
                  },
                  "required": [
                    "salary",
                    "status",
                    "contribPercent",
                    "contribType",
                    "catchUp",
                    "payPeriods"
                  ]
                },
                "result": {
                  "type": "object",
                  "description": "The answer.",
                  "properties": {
                    "takeHome": {
                      "description": "Annual pay after federal tax, Social Security, Medicare and the contribution. No state tax."
                    },
                    "perPaycheck": {
                      "description": "takeHome divided by payPeriods."
                    },
                    "perMonth": {
                      "description": "takeHome divided by 12."
                    },
                    "taxableIncome": {
                      "description": "salary minus pretaxContribution minus the standard deduction, floored at 0. A Roth contribution is not subtracted."
                    },
                    "standardDeduction": {
                      "description": "The 2026 standard deduction for the filing status."
                    },
                    "contribution": {
                      "description": "Dollars contributed over the year, of either type, after the annual limit. Never more than deferralLimit."
                    },
                    "requestedContribution": {
                      "description": "What contribPercent asked for, before the limit: salary times the percent. Equal to contribution unless contributionLimited."
                    },
                    "deferralLimit": {
                      "description": "The § 402(g) cap applied for catchUp: 24500, 32500 or 35750 for 2026. An employer match is outside it."
                    },
                    "contributionLimited": {
                      "description": "Whether the percent asked for more than deferralLimit allows."
                    },
                    "contributionType": {
                      "description": "traditional or roth, echoed back."
                    },
                    "pretaxContribution": {
                      "description": "The part of contribution that reduced taxable income: all of it for traditional, 0 for roth. This is the figure to subtract from a state or local wage base."
                    },
                    "federalIncomeTax": {
                      "description": "Federal income tax on taxableIncome."
                    },
                    "socialSecurity": {
                      "description": "6.2% of wages up to the wage base."
                    },
                    "medicare": {
                      "description": "1.45% of all wages."
                    },
                    "additionalMedicare": {
                      "description": "0.9% of wages above $200,000. Usually 0."
                    },
                    "fica": {
                      "description": "socialSecurity + medicare + additionalMedicare."
                    },
                    "totalTax": {
                      "description": "federalIncomeTax + fica. Federal only."
                    },
                    "effectiveTaxRatePercent": {
                      "description": "totalTax as a percent of gross salary."
                    },
                    "marginalRatePercent": {
                      "description": "The top bracket the income reaches. Not the same as the effective rate."
                    },
                    "brackets": {
                      "description": "Each bracket the income reached: rate, band, the income inside it and the tax on it."
                    },
                    "taxYear": {
                      "description": "The tax year the brackets and deduction come from."
                    },
                    "socialSecurityWageBase": {
                      "description": "Wages above this pay no Social Security tax."
                    },
                    "additionalMedicareThreshold": {
                      "description": "Wages above this pay the extra 0.9% Medicare tax."
                    }
                  }
                },
                "notes": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                },
                "warnings": {
                  "type": "array",
                  "description": "Values that were accepted but may not say what you meant. Percentages here are whole numbers, so 6.5 means 6.5%; a percent parameter sent 0.065 is read as 0.065% and warned about rather than silently rescaled. Absent when there is nothing to say.",
                  "items": {
                    "type": "object",
                    "required": [
                      "param",
                      "code",
                      "message",
                      "sent",
                      "didYouMean"
                    ],
                    "properties": {
                      "param": {
                        "type": "string",
                        "description": "The parameter the warning is about."
                      },
                      "code": {
                        "type": "string",
                        "enum": [
                          "percent_looks_like_fraction"
                        ],
                        "description": "The machine-readable kind. Switch on this rather than on the message."
                      },
                      "message": {
                        "type": "string",
                        "description": "One sentence: what was read, and what to send instead."
                      },
                      "sent": {
                        "type": "number",
                        "description": "The value you sent, as it was parsed."
                      },
                      "didYouMean": {
                        "type": "number",
                        "description": "The value you probably meant — retry with this."
                      }
                    }
                  }
                },
                "meta": {
                  "type": "object",
                  "description": "Provenance: the source site, the matching calculator page, the docs, the disclaimer, and dataVintage — the dated editions this answer depends on.",
                  "properties": {
                    "source": {
                      "type": "string"
                    },
                    "calculator": {
                      "type": "string"
                    },
                    "docs": {
                      "type": "string"
                    },
                    "disclaimer": {
                      "type": "string"
                    },
                    "dataVintage": {
                      "type": "array",
                      "description": "The dated editions behind this answer — figures a publisher set or measured for a named year. Present only where the answer depends on one; arithmetic endpoints omit it.",
                      "items": {
                        "type": "object",
                        "required": [
                          "id",
                          "publisher",
                          "edition",
                          "dataYear",
                          "cycleYears",
                          "supersededFrom",
                          "source"
                        ],
                        "properties": {
                          "id": {
                            "type": "string",
                            "description": "Stable identifier for the edition, matching DATA_VINTAGES in @calcwise/finance."
                          },
                          "publisher": {
                            "type": "string",
                            "description": "The body that measured or set these figures."
                          },
                          "edition": {
                            "type": "string",
                            "description": "The specific publication the figures were transcribed from."
                          },
                          "dataYear": {
                            "type": "integer",
                            "description": "The year the figures describe, or are in force for."
                          },
                          "cycleYears": {
                            "type": "integer",
                            "description": "Years between editions: 1 for an annual series, 3 for the triennial SCF."
                          },
                          "supersededFrom": {
                            "type": "string",
                            "format": "date",
                            "description": "The ISO date (UTC) from which a newer edition is reliably published and this one is a year behind. Compare it against your own clock — this response is cacheable, so it carries no live verdict about its own freshness."
                          },
                          "source": {
                            "type": "string",
                            "description": "The page or notice the current figures were read from."
                          }
                        }
                      }
                    }
                  }
                }
              },
              "required": [
                "endpoint",
                "inputs",
                "result",
                "meta"
              ]
            }
          }
        }
      },
      "state-tax-result": {
        "description": "The state income tax on wages answer.",
        "headers": {
          "Cache-Control": {
            "description": "Long-lived: the same URL is the same answer forever.",
            "schema": {
              "type": "string"
            }
          },
          "X-RateLimit-Remaining": {
            "description": "Requests left in the current window.",
            "schema": {
              "type": "integer"
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "properties": {
                "endpoint": {
                  "type": "string",
                  "const": "state-tax"
                },
                "inputs": {
                  "type": "object",
                  "description": "The parsed inputs the answer was computed from: your request with defaults filled in, money parsed out of strings like \"$400,000\", and enum spellings canonicalised.",
                  "properties": {
                    "state": {
                      "type": "string",
                      "enum": [
                        "AL",
                        "AK",
                        "AZ",
                        "AR",
                        "CA",
                        "CO",
                        "CT",
                        "DE",
                        "DC",
                        "FL",
                        "GA",
                        "HI",
                        "ID",
                        "IL",
                        "IN",
                        "IA",
                        "KS",
                        "KY",
                        "LA",
                        "ME",
                        "MD",
                        "MA",
                        "MI",
                        "MN",
                        "MS",
                        "MO",
                        "MT",
                        "NE",
                        "NV",
                        "NH",
                        "NJ",
                        "NM",
                        "NY",
                        "NC",
                        "ND",
                        "OH",
                        "OK",
                        "OR",
                        "PA",
                        "RI",
                        "SC",
                        "SD",
                        "TN",
                        "TX",
                        "UT",
                        "VT",
                        "VA",
                        "WA",
                        "WV",
                        "WI",
                        "WY"
                      ],
                      "description": "USPS two-letter code, or the full state name (\"Ohio\", \"District of Columbia\"). Where the bare name is taken, the spelling people actually write answers too — \"Washington DC\", \"Washington, D.C.\" and \"D.C.\" for the District, \"Washington State\" and \"New York State\" for the two states whose names those places open. Either case; the code is echoed back."
                    },
                    "wages": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 1000000000,
                      "x-unit": "usd",
                      "description": "Gross annual wages."
                    },
                    "pretaxRetirement": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 1000000000,
                      "x-unit": "usd",
                      "description": "Traditional 401(k)/403(b) dollars contributed over the year. Subtracted from the taxed wages everywhere except Pennsylvania, which taxes elective deferrals. Always echoed; 0 when not sent."
                    },
                    "filingStatus": {
                      "type": "string",
                      "enum": [
                        "single",
                        "married",
                        "hoh",
                        "mfs"
                      ],
                      "description": "How this person files, which decides the state's standard deduction where we model one (Georgia exempts $15,000 single and $30,000 married filing jointly). Ignored in states whose deduction is not modelled. Always echoed; \"single\" when not sent."
                    },
                    "locality": {
                      "type": "string",
                      "enum": [
                        "oh-cincinnati",
                        "oh-cleveland",
                        "oh-columbus",
                        "pa-philadelphia",
                        "pa-philadelphia-nonresident",
                        "mi-detroit",
                        "ny-new-york-city",
                        "in-marion",
                        "in-allen",
                        "in-lake",
                        "in-hamilton",
                        "in-st-joseph",
                        "in-vanderburgh",
                        "in-tippecanoe",
                        "in-monroe",
                        "ky-louisville",
                        "ky-louisville-nonresident",
                        "ky-lexington",
                        "ky-lexington-nonresident",
                        "mi-detroit-nonresident",
                        "mi-grand-rapids",
                        "mi-grand-rapids-nonresident",
                        "mi-lansing",
                        "mi-lansing-nonresident",
                        "mi-flint",
                        "mi-flint-nonresident",
                        "mi-pontiac",
                        "mi-pontiac-nonresident",
                        "mi-east-lansing",
                        "mi-east-lansing-nonresident",
                        "mi-battle-creek",
                        "mi-battle-creek-nonresident",
                        "mi-saginaw",
                        "mi-saginaw-nonresident",
                        "mi-highland-park",
                        "mi-highland-park-nonresident",
                        "mi-muskegon",
                        "mi-muskegon-nonresident",
                        "mi-big-rapids",
                        "mi-big-rapids-nonresident",
                        "mi-walker",
                        "mi-walker-nonresident",
                        "mi-hamtramck",
                        "mi-hamtramck-nonresident",
                        "mi-muskegon-heights",
                        "mi-muskegon-heights-nonresident",
                        "mi-portland",
                        "mi-portland-nonresident",
                        "mi-benton-harbor",
                        "mi-benton-harbor-nonresident",
                        "mi-albion",
                        "mi-albion-nonresident",
                        "mi-ionia",
                        "mi-ionia-nonresident",
                        "mi-lapeer",
                        "mi-lapeer-nonresident",
                        "mi-springfield",
                        "mi-springfield-nonresident",
                        "pa-pittsburgh",
                        "pa-pittsburgh-nonresident",
                        "pa-allentown",
                        "pa-allentown-nonresident",
                        "pa-reading",
                        "pa-reading-nonresident",
                        "pa-erie",
                        "pa-erie-nonresident",
                        "pa-scranton",
                        "pa-scranton-nonresident",
                        "pa-bethlehem",
                        "pa-bethlehem-nonresident",
                        "pa-lancaster",
                        "pa-lancaster-nonresident",
                        "pa-harrisburg",
                        "pa-harrisburg-nonresident",
                        "pa-altoona",
                        "pa-altoona-nonresident",
                        "pa-york",
                        "pa-york-nonresident",
                        "pa-wilkes-barre",
                        "pa-wilkes-barre-nonresident",
                        "pa-state-college",
                        "pa-state-college-nonresident",
                        "pa-norristown",
                        "pa-norristown-nonresident",
                        "pa-west-chester",
                        "pa-west-chester-nonresident",
                        "pa-cheltenham",
                        "pa-cheltenham-nonresident",
                        "pa-upper-darby",
                        "pa-bensalem",
                        "pa-abington",
                        "pa-millcreek",
                        "pa-bristol-township",
                        "pa-ross-township",
                        "pa-mount-lebanon",
                        "oh-toledo",
                        "oh-akron",
                        "oh-dayton",
                        "oh-canton",
                        "oh-springfield",
                        "oh-kettering",
                        "oh-parma",
                        "oh-lorain",
                        "oh-hamilton",
                        "oh-euclid",
                        "oh-cuyahoga-falls",
                        "oh-middletown",
                        "oh-mansfield",
                        "oh-lakewood",
                        "mo-st-louis",
                        "ky-bowling-green"
                      ],
                      "description": "A local wage tax — a city, county, township or borough — to add on top of the state line, where Far Better Off models one. Omit it and every \"local\" field comes back null, which means not asked rather than none — read \"localitiesAvailable\" for what this state offers. The locality must sit in \"state\"; a mismatch is a 400 rather than a silent swap. Echoed only when sent."
                    },
                    "schoolDistrict": {
                      "type": "string",
                      "enum": [
                        "oh-sd-2301-amanda-clearcreek",
                        "oh-sd-0502-athens",
                        "oh-sd-2801-berkshire",
                        "oh-sd-2302-berne-union",
                        "oh-sd-5501-bethel",
                        "oh-sd-1401-blanchester",
                        "oh-sd-7502-botkins",
                        "oh-sd-5901-cardington-lincoln",
                        "oh-sd-5401-celina",
                        "oh-sd-8501-chippewa",
                        "oh-sd-6501-circleville",
                        "oh-sd-7001-clear-fork-valley",
                        "oh-sd-1402-clinton-massie",
                        "oh-sd-5204-cloverleaf",
                        "oh-sd-7201-clyde-green-springs",
                        "oh-sd-1704-crestline",
                        "oh-sd-8702-eastwood",
                        "oh-sd-5101-elgin",
                        "oh-sd-3204-findlay",
                        "oh-sd-0404-geneva-area",
                        "oh-sd-7203-gibsonburg",
                        "oh-sd-8503-green",
                        "oh-sd-3603-greenfield",
                        "oh-sd-0302-hillsdale",
                        "oh-sd-7403-hopewell-loudon",
                        "oh-sd-7506-jackson-center",
                        "oh-sd-6704-james-a-garfield",
                        "oh-sd-4901-jefferson",
                        "oh-sd-4902-jonathan-alder",
                        "oh-sd-8303-kings",
                        "oh-sd-2305-lancaster",
                        "oh-sd-6502-logan-elm",
                        "oh-sd-4904-madison-plains",
                        "oh-sd-5403-marion",
                        "oh-sd-5504-miami-east",
                        "oh-sd-5505-milton-union",
                        "oh-sd-3902-monroeville",
                        "oh-sd-8605-montpelier",
                        "oh-sd-8705-north-baltimore",
                        "oh-sd-4508-north-fork",
                        "oh-sd-1203-northeastern",
                        "oh-sd-4509-northridge",
                        "oh-sd-7612-northwest",
                        "oh-sd-1204-northwestern",
                        "oh-sd-8706-northwood",
                        "oh-sd-7711-norton",
                        "oh-sd-8504-norwayne",
                        "oh-sd-5103-pleasant",
                        "oh-sd-5104-ridgedale",
                        "oh-sd-5105-river-valley",
                        "oh-sd-4604-riverside",
                        "oh-sd-0908-ross",
                        "oh-sd-5008-sebring",
                        "oh-sd-7508-sidney",
                        "oh-sd-3118-southwest",
                        "oh-sd-0604-st-marys",
                        "oh-sd-6503-teays-valley",
                        "oh-sd-7407-tiffin",
                        "oh-sd-6806-tri-county-north",
                        "oh-sd-0505-trimble",
                        "oh-sd-8509-triway",
                        "oh-sd-5509-troy",
                        "oh-sd-2308-walnut-township",
                        "oh-sd-2402-washington-court-house",
                        "oh-sd-2607-wauseon",
                        "oh-sd-2514-westerville",
                        "oh-sd-3907-willard",
                        "oh-sd-7107-zane-trace",
                        "oh-sd-3301-ada",
                        "oh-sd-7501-anna",
                        "oh-sd-1901-ansonia",
                        "oh-sd-6301-antwerp",
                        "oh-sd-3201-arcadia",
                        "oh-sd-1902-arcanum-butler",
                        "oh-sd-3202-arlington",
                        "oh-sd-2001-ayersville",
                        "oh-sd-3901-bellevue",
                        "oh-sd-2501-bexley",
                        "oh-sd-2101-big-walnut",
                        "oh-sd-2303-bloom-carroll",
                        "oh-sd-0203-bluffton",
                        "oh-sd-8701-bowling-green",
                        "oh-sd-5502-bradford",
                        "oh-sd-8601-bryan",
                        "oh-sd-1701-buckeye-central",
                        "oh-sd-2102-buckeye-valley",
                        "oh-sd-2502-canal-winchester",
                        "oh-sd-8801-carey",
                        "oh-sd-8301-carlisle",
                        "oh-sd-2902-cedar-cliff",
                        "oh-sd-4201-centerburg",
                        "oh-sd-2002-central",
                        "oh-sd-1303-clermont-northeastern",
                        "oh-sd-5402-coldwater",
                        "oh-sd-1703-colonel-crawford",
                        "oh-sd-1502-columbiana",
                        "oh-sd-6901-columbus-grove",
                        "oh-sd-6902-continental",
                        "oh-sd-3203-cory-rawson",
                        "oh-sd-5503-covington",
                        "oh-sd-1503-crestview",
                        "oh-sd-8101-crestview",
                        "oh-sd-8502-dalton",
                        "oh-sd-4202-danville",
                        "oh-sd-2003-defiance",
                        "oh-sd-0204-delphos",
                        "oh-sd-6803-eaton",
                        "oh-sd-8602-edgerton",
                        "oh-sd-8703-elmwood",
                        "oh-sd-2602-evergreen",
                        "oh-sd-8001-fairbanks",
                        "oh-sd-2903-fairborn",
                        "oh-sd-2304-fairfield-union",
                        "oh-sd-7503-fairlawn",
                        "oh-sd-2603-fayette",
                        "oh-sd-7504-fort-loramie",
                        "oh-sd-5406-fort-recovery",
                        "oh-sd-1903-franklin-monroe",
                        "oh-sd-7202-fremont",
                        "oh-sd-1305-goshen",
                        "oh-sd-4501-granville",
                        "oh-sd-2904-greeneview",
                        "oh-sd-1904-greenville",
                        "oh-sd-3302-hardin-northern",
                        "oh-sd-7505-hardin-houston",
                        "oh-sd-2004-hicksville",
                        "oh-sd-5902-highland",
                        "oh-sd-3604-hillsboro",
                        "oh-sd-3501-holgate",
                        "oh-sd-6903-jennings",
                        "oh-sd-4503-johnstown-monroe",
                        "oh-sd-6904-kalida",
                        "oh-sd-3303-kenton",
                        "oh-sd-7204-lakota",
                        "oh-sd-6905-leipsic",
                        "oh-sd-3502-liberty-center",
                        "oh-sd-2306-liberty-union-thurston",
                        "oh-sd-3205-liberty-benton",
                        "oh-sd-4506-licking-valley",
                        "oh-sd-4903-london",
                        "oh-sd-0303-loudonville-perrysville",
                        "oh-sd-0905-madison",
                        "oh-sd-3206-mccomb",
                        "oh-sd-1102-mechanicsburg",
                        "oh-sd-8604-millcreek-west-unity",
                        "oh-sd-6906-miller-city-new-cleveland",
                        "oh-sd-0601-minster",
                        "oh-sd-1905-mississinawa-valley",
                        "oh-sd-8802-mohawk",
                        "oh-sd-5903-mount-gilead",
                        "oh-sd-6802-national-trail",
                        "oh-sd-0602-new-bremen",
                        "oh-sd-0603-new-knoxville",
                        "oh-sd-5708-new-lebanon",
                        "oh-sd-3903-new-london",
                        "oh-sd-0907-new-miami",
                        "oh-sd-7404-new-riegel",
                        "oh-sd-4507-newark",
                        "oh-sd-5506-newton",
                        "oh-sd-8003-north-union",
                        "oh-sd-5904-northmor",
                        "oh-sd-8505-northwestern",
                        "oh-sd-3904-norwalk",
                        "oh-sd-4712-oberlin",
                        "oh-sd-7405-old-fort",
                        "oh-sd-8707-otsego",
                        "oh-sd-6907-ottawa-glandorf",
                        "oh-sd-6908-ottoville",
                        "oh-sd-6909-pandora-gilboa",
                        "oh-sd-5405-parkway",
                        "oh-sd-3504-patrick-henry",
                        "oh-sd-6302-paulding",
                        "oh-sd-8708-perrysburg",
                        "oh-sd-2604-pettisville",
                        "oh-sd-2307-pickerington",
                        "oh-sd-2605-pike-delta-york",
                        "oh-sd-5507-piqua",
                        "oh-sd-7007-plymouth-shiloh",
                        "oh-sd-6804-preble-shawnee",
                        "oh-sd-2509-reynoldsburg",
                        "oh-sd-3304-ridgemont",
                        "oh-sd-3305-riverdale",
                        "oh-sd-7507-russia",
                        "oh-sd-7406-seneca-east",
                        "oh-sd-7008-shelby",
                        "oh-sd-3905-south-central",
                        "oh-sd-1205-southeastern",
                        "oh-sd-4510-southwest-licking",
                        "oh-sd-0209-spencerville",
                        "oh-sd-5010-springfield",
                        "oh-sd-8607-stryker",
                        "oh-sd-2606-swanton",
                        "oh-sd-0909-talawanda",
                        "oh-sd-1906-tri-village",
                        "oh-sd-1103-triad",
                        "oh-sd-6805-twin-valley-community",
                        "oh-sd-7106-union-scioto",
                        "oh-sd-1510-united",
                        "oh-sd-8803-upper-sandusky",
                        "oh-sd-3306-upper-scioto-valley",
                        "oh-sd-5713-valley-view",
                        "oh-sd-3207-van-buren",
                        "oh-sd-8104-van-wert",
                        "oh-sd-3208-vanlue",
                        "oh-sd-1907-versailles",
                        "oh-sd-0605-wapakoneta",
                        "oh-sd-6303-wayne-trace",
                        "oh-sd-0606-waynesfield-goshen",
                        "oh-sd-4715-wellington",
                        "oh-sd-1105-west-liberty-salem",
                        "oh-sd-3906-western-reserve",
                        "oh-sd-3122-wyoming",
                        "oh-sd-2906-xenia-community",
                        "oh-sd-2907-yellow-springs"
                      ],
                      "description": "Ohio's school district income tax, which is a second local levy on the same paycheck and the only one here charged by where you **live** rather than where you work — so it is owed on top of \"locality\" and not instead of it. All 214 districts that levy one for 2026 are accepted, by id or by the four-digit code from box 20 of your W-2 (\"2301\"), which is the only reliable way to match one: a district's boundaries are not a city's, a township's or a ZIP code's, so a bare name is deliberately not a spelling. Omit it and every \"schoolDistrict\" field comes back null, which means not asked rather than none — read \"schoolDistrictsAvailable\" for what this state offers. The district must sit in \"state\"; a mismatch is a 400 rather than a silent swap. Echoed only when sent."
                    },
                    "localExemptions": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 20,
                      "description": "Personal and dependency exemptions to claim against the locality's tax, as a head count — the filer, a spouse, each dependent. Only Michigan's 40 rates allow any of the localities modelled ($600 a head in Detroit, Grand Rapids, Lansing, Flint, Pontiac, East Lansing, Highland Park, Muskegon, Big Rapids, Walker, Hamtramck, Muskegon Heights, Albion and Lapeer, and $700 a head in Ionia, and $750 a head in Battle Creek, Saginaw, Benton Harbor and Springfield, and $1000 a head in Portland, read back in \"localExemptionAmount\"); elsewhere it changes nothing. The default of 0 is the larger bill, so an omitted count never understates a city. Always echoed; 0 when not sent."
                    }
                  },
                  "required": [
                    "state",
                    "wages",
                    "pretaxRetirement",
                    "filingStatus",
                    "localExemptions"
                  ]
                },
                "result": {
                  "type": "object",
                  "description": "The answer.",
                  "properties": {
                    "state": {
                      "description": "The USPS code the answer is for."
                    },
                    "stateName": {
                      "description": "The state's full name."
                    },
                    "taxability": {
                      "description": "Which of four cases this state is: \"none\" (taxes no wages), \"flat\" (one verified statutory rate), \"graduated\" (a verified bracket schedule, applied in full — \"ratePercent\" is null because no single rate describes it) or \"unmodeled\" (brackets Far Better Off has not verified, where there is no answer).",
                      "type": "string",
                      "enum": [
                        "none",
                        "flat",
                        "graduated",
                        "unmodeled"
                      ]
                    },
                    "modeled": {
                      "description": "True when tax is a number. False only where taxability is \"unmodeled\"."
                    },
                    "tax": {
                      "description": "State income tax on the year's wages, or null where taxability is \"unmodeled\". Null is not zero."
                    },
                    "taxedWages": {
                      "description": "The wages a rate was applied to: wages minus pretaxRetirement (the whole salary in Pennsylvania), minus standardDeduction where one is modelled, minus ficaDeduction in Massachusetts. 0 where the state taxes no wages at all, null where there is no answer."
                    },
                    "standardDeduction": {
                      "description": "What this state exempts for this filingStatus before its rate applies, where we have verified it. Null usually means not verified, never verified as zero — but in Pennsylvania it means none exists, and in Utah it means the state grants none and hands the federal deduction back as a credit instead (see \"credit\"). It is what these wages actually got, not a table row: Illinois disallows its exemption allowance outright over $250,000 ($500,000 filing jointly), so a filer above the limit gets 0 with the label still set."
                    },
                    "standardDeductionLabel": {
                      "description": "What the state calls it: \"standard deduction\", or \"personal exemption and standard deduction\" in Mississippi, which gives both. Null where none is modelled."
                    },
                    "standardDeductionSource": {
                      "description": "Where the deduction figures were verified, when that isn't the same page as the rate (North Carolina's are in the statute, Kentucky's in a DOR announcement). Null otherwise."
                    },
                    "ficaDeduction": {
                      "description": "What this state takes off for the Social Security and Medicare tax already paid on these wages — Massachusetts's, and null in every other state, where the answer is that no such deduction exists rather than that nobody checked. M.G.L. c. 62, § 3(B)(a)(3) caps it at $2,000 \"attributable to any one taxpayer\", so every salary over $26,144 gets exactly $2,000 and a smaller one gets the 7.65% it actually paid. It follows the payroll tax rather than the income tax, so pretaxRetirement does not reduce it, and filingStatus does not double it: the second $2,000 belongs to a second earner, whose pay this endpoint was not given. taxedWages already has it subtracted."
                    },
                    "ficaDeductionLabel": {
                      "description": "What the state calls it, or null. Massachusetts: \"deduction for the Social Security and Medicare tax you paid\"."
                    },
                    "ficaDeductionSource": {
                      "description": "Where that cap and what it is taken of were verified. Null in every state that has no such deduction."
                    },
                    "filingStatus": {
                      "description": "The filing status the answer used. Echoed so a cached response is self-describing."
                    },
                    "effectiveRatePercent": {
                      "description": "tax as a percent of gross wages. Lower than ratePercent wherever there is an exempt band."
                    },
                    "ratePercent": {
                      "description": "The statutory rate on wage income, or null — null in a graduated state too, where the answer is a table rather than a rate. Read \"rateLabel\" for how the state states it, and \"effectiveRatePercent\" for what these wages actually paid."
                    },
                    "marginalRatePercent": {
                      "description": "The rate this filer's next dollar of wages meets, as a percent — the number that answers \"what does a raise cost me here\", which effectiveRatePercent and ratePercent both get wrong. 0 in a state that taxes no wages and 0 under a filing threshold or inside an exempt band, the statutory rate in a flat state, the band this income reaches in a graduated one, and null only where taxability is \"unmodeled\". It can be **higher** than ratePercent: Massachusetts adds its 4-point surtax above the threshold, and Utah's tapering credit costs another 1.3 cents a dollar while it lasts, so a Utahn inside that taper meets 5.8% and not the 4.5% the state advertises. Clamping this to the statutory rate would be wrong in both states."
                    },
                    "marginalBracket": {
                      "description": "The band marginalRatePercent came from, as { over, ratePercent }, in a state that publishes a schedule. Null in a flat state (ratePercent is the whole answer), in a state that taxes no wages, and under a filing threshold, where no band applies because no tax does."
                    },
                    "brackets": {
                      "description": "This state's own schedule for this filingStatus, as [{ over, ratePercent }] from the first dollar — the table \"tax\" was worked out from, so a caller can reproduce or display it without a second source. Null in a flat state and where the state taxes no wages. The bands are measured on taxable income: taxedWages less exemptBelow, not on gross wages. A schedule alone does not always reproduce \"tax\" — read \"supplementalTax\" in New York, \"baseTax\" in Ohio and \"credit\" in Utah."
                    },
                    "stateAndLocalMarginalRatePercent": {
                      "description": "marginalRatePercent plus localMarginalRatePercent: what one more dollar of wages really costs in sub-federal income tax. Null wherever either half is null — an unmodeled state, or no locality asked for — for the same reason stateAndLocalTax is. The two add honestly because every locality modelled taxes a base that a dollar of wages enters: gross pay in Ohio's cities and Philadelphia, federal AGI in Detroit, and state taxable income in New York City and Indiana's counties."
                    },
                    "exemptBelow": {
                      "description": "The slab this state taxes at nothing before its rate starts, after any standardDeduction (Ohio, Mississippi, Idaho). For this filingStatus: Idaho's is $4,811 on a single return and $9,622 on a joint one. Null elsewhere."
                    },
                    "noTaxBelow": {
                      "description": "The filing threshold this state charges nothing at all under, for this filingStatus — Virginia's $11,950 ($23,900 filing jointly) and New Jersey's $10,000 ($20,000 jointly or as head of household). Null in every other state. Unlike exemptBelow this is measured on income before any standardDeduction, and clearing it does not subtract it: a dollar over, the whole schedule applies from the bottom."
                    },
                    "belowFilingThreshold": {
                      "description": "Whether these taxedWages fall under noTaxBelow, so the state charges nothing. The two states disagree about the boundary itself and this answers for each: Virginia's statute says \"less than $11,950\", so $11,950 exactly is taxed, while New Jersey's says \"$10,000 or less ... pay no tax\", so $10,000 exactly is not. False in a state with no threshold, null where the state is unmodeled."
                    },
                    "baseTax": {
                      "description": "Flat dollars owed once the exempt band is cleared (Ohio's $332). Null elsewhere."
                    },
                    "surtaxPercent": {
                      "description": "Extra percentage points a state charges above a threshold, on top of ratePercent (Massachusetts: 4). Null in every other state."
                    },
                    "surtaxThreshold": {
                      "description": "The taxable income above which surtaxPercent applies. Massachusetts indexes it annually and does not vary it by filing status, so a couple filing jointly reaches it at the same income a single filer does. Null elsewhere."
                    },
                    "surtax": {
                      "description": "How much of tax came from that top band for these wages — 0 below the threshold, and null in a state that has no surtax. tax already includes it; this is here so ratePercent times taxedWages and our number agree."
                    },
                    "surtaxSource": {
                      "description": "Where the surtax rate and its threshold were verified. Null where there is no surtax."
                    },
                    "supplementalTax": {
                      "description": "How much of tax came from a state taking back the benefit of its own lower brackets — New York's supplemental tax, and nothing else here. 0 below the income where it starts, and null in a state that has no such rule. tax already includes it; this is here so a consumer applying the bracket schedule and our number agree."
                    },
                    "supplementalTaxLabel": {
                      "description": "How the state describes that rule, or null. New York: \"New York's supplemental tax, which takes back the lower brackets above $107,650\"."
                    },
                    "supplementalTaxSource": {
                      "description": "Where those steps were verified. Null where the state has no such rule."
                    },
                    "credit": {
                      "description": "A credit this state takes off the tax itself, for these wages — Utah's taxpayer tax credit, and nothing else here. It is 6% of the federal standard deduction, less 1.3% of income above a base amount, so it falls to 0 around $92,500 of single income. tax already has it subtracted and is floored at zero, so a low earner's credit can be larger than the tax it wiped out. Null in every other state."
                    },
                    "creditLabel": {
                      "description": "What the state calls it, or null. Utah: \"taxpayer tax credit\"."
                    },
                    "creditSource": {
                      "description": "Where the credit's percentages and base amounts were verified. Null elsewhere."
                    },
                    "taxesPretaxRetirement": {
                      "description": "True where 401(k) contributions are taxed as compensation anyway (Pennsylvania)."
                    },
                    "rateLabel": {
                      "description": "How the state describes its own rate, in a phrase you can print."
                    },
                    "excludes": {
                      "description": "What this figure leaves out in this state — deductions, exemptions, credits, local taxes. Always read it."
                    },
                    "reason": {
                      "description": "Present only where taxability is \"unmodeled\": why there is no number."
                    },
                    "source": {
                      "description": "Where this state's figure was verified: { publisher, url, verified?, quote? } on the agency's, legislature's or code's own site. \"verified\" is an ISO day that citation was last re-opened and found to still say this, and it is absent rather than null where nobody has re-read it — which is not a doubtful rate, only an unrepeated reading. \"quote\" is the sentence on that page the day was earned on, so you can repeat the check instead of trusting it: fetch the url, strip tags to single spaces, fold curly punctuation to ASCII, and the string is still there. It is absent with no \"verified\" day, and absent on the two citations that are PDFs, where no substring search can repeat a person's reading. Null only where a rate is confirmed but not yet linked."
                    },
                    "taxYear": {
                      "description": "The tax year the rates come from."
                    },
                    "localitiesAvailable": {
                      "description": "Every local wage tax Far Better Off models in this state, whether a city, county, township or borough levies it, as { id, name, ratePercent, topRatePercent } — the accepted values of \"locality\" here, so a client can build the picker without a second call. \"ratePercent\" is null for a locality that publishes a schedule instead of a rate (New York City's four resident brackets), and \"topRatePercent\" is its highest rate, so a picker always has one number to show. Empty is \"none modelled\", never \"none exists\": Indiana's other 84 counties, many Kentucky and Michigan cities and much of Pennsylvania levy one that is not in this table yet, and \"excludes\" says so."
                    },
                    "locality": {
                      "description": "The locality id the answer used, or null when none was asked for."
                    },
                    "localityName": {
                      "description": "What that locality is called (\"Philadelphia (I live in the city)\"). Null when none was asked for."
                    },
                    "localTax": {
                      "description": "Local income tax — city, county, township or borough — on the same year of wages, or null when no locality was asked for. Null is not zero, exactly as it is for the state line."
                    },
                    "localRatePercent": {
                      "description": "The locality's rate on wage income, as a percent. Null when none was asked for, and null in New York City, which publishes a schedule rather than a rate — read \"localBrackets\" and \"localMarginalRatePercent\" there."
                    },
                    "localMarginalRatePercent": {
                      "description": "The rate on the next dollar of local tax, as a percent — the locality's flat rate everywhere but New York City, and the bracket this filer's city taxable income lands in there. Null when no locality was asked for."
                    },
                    "localBrackets": {
                      "description": "The locality's own schedule for this filing status, as [{ over, ratePercent }] from the first dollar, where it publishes one — New York City only. Null everywhere else, where there is a single \"localRatePercent\" instead."
                    },
                    "localTaxableIncome": {
                      "description": "What the locality's rate or schedule was applied to, where that is not gross pay: New York City charges its brackets on New York State taxable income and an Indiana county charges its rate on Indiana's, so this is the state's standard deduction or personal exemption already taken off. Null for every locality that taxes gross pay (read \"localTax\" and \"localRatePercent\" there) and when none was asked for."
                    },
                    "localTaxLabel": {
                      "description": "How the locality states its own charge, in a phrase you can print (\"Philadelphia's 3.735% resident Wage Tax\"). Null when none was asked for."
                    },
                    "localTaxesPretaxRetirement": {
                      "description": "True where the locality taxes 401(k) elective deferrals as compensation anyway — true in Ohio's cities, which tax box 5 Medicare wages, and in Philadelphia, which follows the Pennsylvania rule. False in Michigan's 40, whose base is a federal figure the contribution was never in — the AGI on a Detroit resident's return, W-2 box 1 in every other Michigan city here and on a Detroit commuter's. Null when no locality was asked for."
                    },
                    "localExemptionAmount": {
                      "description": "Dollars a year this locality exempts for each personal or dependency exemption, resident and non-resident alike — $600 a head in Detroit, Grand Rapids, Lansing, Flint, Pontiac, East Lansing, Highland Park, Muskegon, Big Rapids, Walker, Hamtramck, Muskegon Heights, Albion and Lapeer, and $700 a head in Ionia, and $750 a head in Battle Creek, Saginaw, Benton Harbor and Springfield, and $1000 a head in Portland. Each is the figure that city prints on its own return rather than one the state sets: the Uniform City Income Tax Ordinance caps the rate and says nothing about the exemption, which is why Battle Creek's is the largest of them. Null where the locality allows none, which is every locality outside Michigan, and null when no locality was asked for. Null here is \"the city taxes the first dollar\", not \"unverified\"."
                    },
                    "localExemptionsApplied": {
                      "description": "The exemption count actually used, which is 0 wherever the locality allows none however many you asked for. Null when no locality was asked for."
                    },
                    "localExcludes": {
                      "description": "What the local figure leaves out — a second Ohio municipality's tax net of credit, Philadelphia's income-based refund. Always read it. Null when no locality was asked for."
                    },
                    "localSource": {
                      "description": "Where the locality's rate was verified: { publisher, url } on the city's own revenue page. Null when no locality was asked for."
                    },
                    "schoolDistrictsAvailable": {
                      "description": "Every school district income tax Far Better Off models in this state, as { id, code, name, ratePercent, base } — the accepted values of \"schoolDistrict\", so a client can build the second picker without a second call. \"code\" is the four-digit number box 20 of a W-2 carries, which is the field to match a reader against; \"base\" is \"wages\" or \"state taxable income\", and both appear in Ohio, which is why one rate is two bills. Empty in 50 of the 51 jurisdictions, and that is \"none modelled\", never \"none exists\": Pennsylvania's school districts levy an earned income tax of their own, which rides inside the Act 32 rates in \"localitiesAvailable\" rather than as rows here."
                    },
                    "schoolDistrict": {
                      "description": "The school district id the answer used, or null when none was asked for."
                    },
                    "schoolDistrictName": {
                      "description": "What that district is called, with its four-digit code (\"Westerville City School District (2512)\"). Null when none was asked for."
                    },
                    "schoolDistrictCode": {
                      "description": "The four-digit code alone, so a client can show it against box 20 of the reader's own W-2 without parsing the name. Null when no district was asked for."
                    },
                    "schoolDistrictTax": {
                      "description": "Ohio school district income tax on the same year of wages, or null when no district was asked for. Null is not zero, exactly as it is for the state and local lines. It is charged on where the reader lives, so it is owed **on top of** localTax, which is charged on where they work — a Westerville resident working in Columbus owes both."
                    },
                    "schoolDistrictRatePercent": {
                      "description": "The district's rate on its own base, as a percent. Every district modelled charges one flat rate — none publishes a schedule — so unlike localRatePercent this is never null for a district that was asked for."
                    },
                    "schoolDistrictBase": {
                      "description": "Which of the two bases Ohio Revised Code 5748.01(E) lets a district choose this one is on: \"wages\" for the earned income base, division (E)(2), which is wages with no deduction and no exemption, or \"state taxable income\" for the traditional base, division (E)(1), which is Form SD 100 line 5 — modified adjusted gross income less the Ohio IT 1040 exemption. It is the field that explains why one rate is two bills: 1% is $750 on the earned income base and $729 on the traditional one for a single filer on $75,000, and $708 filing jointly, because the traditional base takes two of Ohio's exemptions and the earned income base has no filing status at all. Null when no district was asked for."
                    },
                    "schoolDistrictTaxableIncome": {
                      "description": "What the district's rate was applied to, where that is not gross pay — the Ohio taxable income a traditional-base district charges. Ohio's $26,050 zero band and its $332 of flat dollars are **not** in it: they belong to the state's own schedule, which is why $28,400 of wages can owe Ohio nothing and owe a 1% district $260.50. Null for an earned income district, where the answer is \"wages\", and null when none was asked for."
                    },
                    "schoolDistrictTaxLabel": {
                      "description": "How the district states its own charge, in a phrase you can print (\"Westerville City School District's 0.75% income tax\"). Null when none was asked for."
                    },
                    "schoolDistrictTaxesPretaxRetirement": {
                      "description": "False for every district modelled, and it is the one local line in Ohio where that is true: a municipal tax here reaches \"qualifying wages\" — box 5 Medicare wages, which an elective deferral is still inside — while both school district bases are built on federal adjusted gross income, which it was never in. So on one Ohio payslip a 5% deferral leaves Columbus at $1,875 and takes Westerville from $562.50 to $534.38. Null when no district was asked for."
                    },
                    "schoolDistrictExcludes": {
                      "description": "What the district figure leaves out — self-employment earnings on the earned income base, every non-wage dollar of modified adjusted gross income on the traditional one, Ohio's dependency exemptions, and the SD 100's $50 senior citizen credit. Always read it. Null when none was asked for."
                    },
                    "schoolDistrictSource": {
                      "description": "Where the district's rate and its base were verified: { publisher, url } on the Ohio Department of Taxation's own employer-withholding list. Null when none was asked for."
                    },
                    "schoolDistrictTaxYear": {
                      "description": "The year the district rates are in force for. Ohio's list is dated by the day employers must use it — 1 January — rather than by a tax year, so it is stated separately from taxYear above. Null when no district was asked for."
                    },
                    "stateAndLocalTax": {
                      "description": "Every sub-federal income tax line on this salary added up: \"tax\" plus \"localTax\" plus \"schoolDistrictTax\", which is the number a person here actually loses. Null wherever any half asked for is null — an unmodeled state, or no locality and no district asked for — because a sum missing a term is not a total. A term nobody asked for is not missing: with \"locality\" alone this is tax plus localTax, as it always was, and the district only enters it when a district was named."
                    },
                    "payrollContributions": {
                      "description": "What this state withholds from the employee's own wages for a named state programme in a year — disability insurance, paid family and medical leave, and in New Jersey unemployment insurance too. It is not income tax, so no rate above can show it, and it is often the larger deduction: California's SDI takes 1.3% of every dollar earned, $975 a year on $75,000, where the state income tax on the same salary is under 4%. Charged on gross wages, so pretaxRetirement does not reduce it — a 401(k) deferral escapes the income tax and not this. Null where Far Better Off has verified no such contribution, which is not zero: Delaware's Paid Leave and Maryland's FAMLI both belong here and neither rate has been read off its agency's page yet."
                    },
                    "payrollContributionLabel": {
                      "description": "The line a pay stub shows, in a phrase you can print (\"California SDI\", \"Washington PFML + WA Cares\"). Null where none is modelled."
                    },
                    "payrollContributionDescription": {
                      "description": "The same thing in running text, for a sentence rather than a table cell (\"State Disability Insurance\"). Null where none is modelled."
                    },
                    "payrollContributionRatePercent": {
                      "description": "Every programme's employee rate added up, as a percent. Read it with \"payrollContributionPrograms\": the sum describes a salary under the lowest wage base and no other, because New Jersey's four rates stop at two different ceilings — Unemployment Insurance and workforce development at $44,800 — so above that the real charge is less than this figure states. Null where none is modelled."
                    },
                    "payrollContributionMarginalRatePercent": {
                      "description": "The rate the next dollar of wages meets in contributions, as a percent — the figure to add to marginalRatePercent when answering what a raise costs. It is 0 once every wage base is passed (a Rhode Islander over $100,000 pays no more Temporary Disability Insurance however big the raise), the whole rate below the lowest base, and in between it is neither: a New Jerseyan over $44,800 meets 0.42% of the 0.845% the rate above sums, because Unemployment Insurance has stopped charging and disability and family leave have not. Null where none is modelled."
                    },
                    "payrollContributionPrograms": {
                      "description": "The programmes behind the total, in the order a pay stub lists them, as [{ name, short, ratePercent, wageBase, chargedWages, amount }]. \"wageBase\" is null where the programme charges every dollar — California removed SDI's ceiling on 1 January 2024 and WA Cares never had one — and \"chargedWages\" is what the rate actually met, so a base that has bitten is visible rather than inferred. Null where none is modelled."
                    },
                    "payrollContributionYear": {
                      "description": "The calendar year these rates are in force for. They turn over on 1 January, unlike the income tax rates above, which are stated for a tax year. Null where none is modelled."
                    },
                    "payrollContributionExcludes": {
                      "description": "What the contribution figure leaves out or assumes, in the state's own terms — an employer that volunteers to pay more of your share, a private plan that withholds instead, a WA Cares exemption, a rate already legislated to move next January. Always read it. Null where none is modelled."
                    },
                    "payrollContributionSource": {
                      "description": "Where each rate and wage base was verified: { publisher, url } on the labor or paid-leave agency's own page. Null where none is modelled."
                    },
                    "stateTaxAndContributions": {
                      "description": "Everything the state itself takes out of this salary: \"tax\" plus \"payrollContributions\". Null wherever either half is null — an unmodeled state, or one with no verified contribution — for the same reason \"stateAndLocalTax\" is null without a locality: a sum missing a term is not a total. It excludes any locality; add \"localTax\" for the whole sub-federal bill."
                    }
                  }
                },
                "notes": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                },
                "meta": {
                  "type": "object",
                  "description": "Provenance: the source site, the matching calculator page, the docs, the disclaimer, and dataVintage — the dated editions this answer depends on.",
                  "properties": {
                    "source": {
                      "type": "string"
                    },
                    "calculator": {
                      "type": "string"
                    },
                    "docs": {
                      "type": "string"
                    },
                    "disclaimer": {
                      "type": "string"
                    },
                    "dataVintage": {
                      "type": "array",
                      "description": "The dated editions behind this answer — figures a publisher set or measured for a named year. Present only where the answer depends on one; arithmetic endpoints omit it.",
                      "items": {
                        "type": "object",
                        "required": [
                          "id",
                          "publisher",
                          "edition",
                          "dataYear",
                          "cycleYears",
                          "supersededFrom",
                          "source"
                        ],
                        "properties": {
                          "id": {
                            "type": "string",
                            "description": "Stable identifier for the edition, matching DATA_VINTAGES in @calcwise/finance."
                          },
                          "publisher": {
                            "type": "string",
                            "description": "The body that measured or set these figures."
                          },
                          "edition": {
                            "type": "string",
                            "description": "The specific publication the figures were transcribed from."
                          },
                          "dataYear": {
                            "type": "integer",
                            "description": "The year the figures describe, or are in force for."
                          },
                          "cycleYears": {
                            "type": "integer",
                            "description": "Years between editions: 1 for an annual series, 3 for the triennial SCF."
                          },
                          "supersededFrom": {
                            "type": "string",
                            "format": "date",
                            "description": "The ISO date (UTC) from which a newer edition is reliably published and this one is a year behind. Compare it against your own clock — this response is cacheable, so it carries no live verdict about its own freshness."
                          },
                          "source": {
                            "type": "string",
                            "description": "The page or notice the current figures were read from."
                          }
                        }
                      }
                    }
                  }
                }
              },
              "required": [
                "endpoint",
                "inputs",
                "result",
                "meta"
              ]
            }
          }
        }
      },
      "loan-result": {
        "description": "The loan payment & amortization answer.",
        "headers": {
          "Cache-Control": {
            "description": "Long-lived: the same URL is the same answer forever.",
            "schema": {
              "type": "string"
            }
          },
          "X-RateLimit-Remaining": {
            "description": "Requests left in the current window.",
            "schema": {
              "type": "integer"
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "properties": {
                "endpoint": {
                  "type": "string",
                  "const": "loan"
                },
                "inputs": {
                  "type": "object",
                  "description": "The parsed inputs the answer was computed from: your request with defaults filled in, money parsed out of strings like \"$400,000\", and enum spellings canonicalised.",
                  "properties": {
                    "principal": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 100000000000,
                      "x-unit": "usd",
                      "description": "Amount borrowed."
                    },
                    "rate": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 100,
                      "x-unit": "percent",
                      "description": "Annual interest rate (APR)."
                    },
                    "termMonths": {
                      "type": "integer",
                      "minimum": 1,
                      "maximum": 1200,
                      "x-unit": "months",
                      "description": "Length of the loan in months. A 30-year mortgage is 360."
                    },
                    "extraMonthly": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 1000000000,
                      "x-unit": "usd",
                      "description": "Extra principal paid on top of the scheduled payment each month. Always echoed; 0 when not sent."
                    },
                    "schedule": {
                      "type": "boolean",
                      "description": "Include the year-by-year amortization schedule in the response. Always echoed; false when not sent."
                    }
                  },
                  "required": [
                    "principal",
                    "rate",
                    "termMonths",
                    "extraMonthly",
                    "schedule"
                  ]
                },
                "result": {
                  "type": "object",
                  "description": "The answer.",
                  "properties": {
                    "payment": {
                      "description": "Scheduled monthly payment (principal and interest only)."
                    },
                    "months": {
                      "description": "Months until the loan is paid off, after any extra principal."
                    },
                    "termLabel": {
                      "description": "That payoff time in words, e.g. \"24 yr 11 mo\"."
                    },
                    "totalInterest": {
                      "description": "Interest paid over the life of the loan."
                    },
                    "totalPaid": {
                      "description": "Principal plus interest."
                    },
                    "extraPayment": {
                      "description": "Present only when extraMonthly > 0: months and interest saved against the original schedule."
                    },
                    "schedule": {
                      "description": "Present only when schedule=true: one row per year with principal paid, interest paid and closing balance."
                    }
                  }
                },
                "warnings": {
                  "type": "array",
                  "description": "Values that were accepted but may not say what you meant. Percentages here are whole numbers, so 6.5 means 6.5%; a percent parameter sent 0.065 is read as 0.065% and warned about rather than silently rescaled. Absent when there is nothing to say.",
                  "items": {
                    "type": "object",
                    "required": [
                      "param",
                      "code",
                      "message",
                      "sent",
                      "didYouMean"
                    ],
                    "properties": {
                      "param": {
                        "type": "string",
                        "description": "The parameter the warning is about."
                      },
                      "code": {
                        "type": "string",
                        "enum": [
                          "percent_looks_like_fraction"
                        ],
                        "description": "The machine-readable kind. Switch on this rather than on the message."
                      },
                      "message": {
                        "type": "string",
                        "description": "One sentence: what was read, and what to send instead."
                      },
                      "sent": {
                        "type": "number",
                        "description": "The value you sent, as it was parsed."
                      },
                      "didYouMean": {
                        "type": "number",
                        "description": "The value you probably meant — retry with this."
                      }
                    }
                  }
                },
                "meta": {
                  "type": "object",
                  "description": "Provenance: the source site, the matching calculator page, the docs and the disclaimer.",
                  "properties": {
                    "source": {
                      "type": "string"
                    },
                    "calculator": {
                      "type": "string"
                    },
                    "docs": {
                      "type": "string"
                    },
                    "disclaimer": {
                      "type": "string"
                    }
                  }
                }
              },
              "required": [
                "endpoint",
                "inputs",
                "result",
                "meta"
              ]
            }
          }
        }
      },
      "mortgage-result": {
        "description": "The mortgage payment (piti) answer.",
        "headers": {
          "Cache-Control": {
            "description": "Long-lived: the same URL is the same answer forever.",
            "schema": {
              "type": "string"
            }
          },
          "X-RateLimit-Remaining": {
            "description": "Requests left in the current window.",
            "schema": {
              "type": "integer"
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "properties": {
                "endpoint": {
                  "type": "string",
                  "const": "mortgage"
                },
                "inputs": {
                  "type": "object",
                  "description": "The parsed inputs the answer was computed from: your request with defaults filled in, money parsed out of strings like \"$400,000\", and enum spellings canonicalised.",
                  "properties": {
                    "homePrice": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 10000000000,
                      "x-unit": "usd",
                      "description": "Purchase price."
                    },
                    "downPayment": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 10000000000,
                      "x-unit": "usd",
                      "description": "Down payment in dollars. Always echoed; 0 when not sent."
                    },
                    "rate": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 100,
                      "x-unit": "percent",
                      "description": "Annual interest rate (APR)."
                    },
                    "termYears": {
                      "type": "integer",
                      "minimum": 1,
                      "maximum": 50,
                      "x-unit": "years",
                      "description": "Loan term in years. Always echoed; 30 when not sent."
                    },
                    "propertyTaxAnnual": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 10000000,
                      "x-unit": "usd",
                      "description": "Property tax per year. Always echoed; 0 when not sent."
                    },
                    "insuranceAnnual": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 10000000,
                      "x-unit": "usd",
                      "description": "Homeowner's insurance per year. Always echoed; 0 when not sent."
                    },
                    "hoaMonthly": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 1000000,
                      "x-unit": "usd",
                      "description": "HOA dues per month. Always echoed; 0 when not sent."
                    },
                    "pmiRate": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 5,
                      "x-unit": "percent",
                      "description": "Annual PMI premium as a percentage of the loan. Only charged below 20% down. Freddie Mac puts a typical premium at $30–$70 a month per $100,000 borrowed, which is 0.36%–0.84% of the loan a year. Always echoed; 0.5 when not sent."
                    },
                    "extraMonthly": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 10000000,
                      "x-unit": "usd",
                      "description": "Extra principal per month. Always echoed; 0 when not sent."
                    }
                  },
                  "required": [
                    "homePrice",
                    "downPayment",
                    "rate",
                    "termYears",
                    "propertyTaxAnnual",
                    "insuranceAnnual",
                    "hoaMonthly",
                    "pmiRate",
                    "extraMonthly"
                  ]
                },
                "result": {
                  "type": "object",
                  "description": "The answer.",
                  "properties": {
                    "loanAmount": {
                      "description": "Home price minus the down payment."
                    },
                    "downPaymentPercent": {
                      "description": "Down payment as a percentage of the price."
                    },
                    "principalAndInterest": {
                      "description": "The loan payment alone."
                    },
                    "monthlyTotal": {
                      "description": "Everything due each month, including any extra principal."
                    },
                    "breakdown": {
                      "description": "Each component of the monthly total, in dollars."
                    },
                    "totalInterest": {
                      "description": "Interest over the life of the loan, after any extra principal."
                    },
                    "payoffMonths": {
                      "description": "Months to payoff."
                    },
                    "payoffTermLabel": {
                      "description": "That payoff time in words, e.g. \"30 yr\" or \"24 yr 11 mo\"."
                    },
                    "pmi": {
                      "description": "Null when no PMI is charged. Otherwise the month it drops off automatically, the earlier month you may request cancellation, and what it costs either way."
                    }
                  }
                },
                "notes": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                },
                "warnings": {
                  "type": "array",
                  "description": "Values that were accepted but may not say what you meant. Percentages here are whole numbers, so 6.5 means 6.5%; a percent parameter sent 0.065 is read as 0.065% and warned about rather than silently rescaled. Absent when there is nothing to say.",
                  "items": {
                    "type": "object",
                    "required": [
                      "param",
                      "code",
                      "message",
                      "sent",
                      "didYouMean"
                    ],
                    "properties": {
                      "param": {
                        "type": "string",
                        "description": "The parameter the warning is about."
                      },
                      "code": {
                        "type": "string",
                        "enum": [
                          "percent_looks_like_fraction"
                        ],
                        "description": "The machine-readable kind. Switch on this rather than on the message."
                      },
                      "message": {
                        "type": "string",
                        "description": "One sentence: what was read, and what to send instead."
                      },
                      "sent": {
                        "type": "number",
                        "description": "The value you sent, as it was parsed."
                      },
                      "didYouMean": {
                        "type": "number",
                        "description": "The value you probably meant — retry with this."
                      }
                    }
                  }
                },
                "meta": {
                  "type": "object",
                  "description": "Provenance: the source site, the matching calculator page, the docs and the disclaimer.",
                  "properties": {
                    "source": {
                      "type": "string"
                    },
                    "calculator": {
                      "type": "string"
                    },
                    "docs": {
                      "type": "string"
                    },
                    "disclaimer": {
                      "type": "string"
                    }
                  }
                }
              },
              "required": [
                "endpoint",
                "inputs",
                "result",
                "meta"
              ]
            }
          }
        }
      },
      "house-affordability-result": {
        "description": "The how much house can i afford answer.",
        "headers": {
          "Cache-Control": {
            "description": "Long-lived: the same URL is the same answer forever.",
            "schema": {
              "type": "string"
            }
          },
          "X-RateLimit-Remaining": {
            "description": "Requests left in the current window.",
            "schema": {
              "type": "integer"
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "properties": {
                "endpoint": {
                  "type": "string",
                  "const": "house-affordability"
                },
                "inputs": {
                  "type": "object",
                  "description": "The parsed inputs the answer was computed from: your request with defaults filled in, money parsed out of strings like \"$400,000\", and enum spellings canonicalised.",
                  "properties": {
                    "income": {
                      "type": "number",
                      "minimum": 1,
                      "maximum": 100000000,
                      "x-unit": "usd",
                      "description": "Gross household income per year, before tax. Annual, not monthly — the ratios below are applied to a twelfth of it."
                    },
                    "monthlyDebts": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 1000000,
                      "x-unit": "usd",
                      "description": "Required monthly payments on everything that is not housing — car, student loans, credit-card minimums, alimony and child support. Groceries, utilities and phone bills are not debt and do not belong here. Always echoed; 0 when not sent."
                    },
                    "downPayment": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 100000000,
                      "x-unit": "usd",
                      "description": "Cash going in. It raises the price you can reach and is not borrowed. Always echoed; 0 when not sent."
                    },
                    "rate": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 100,
                      "x-unit": "percent",
                      "description": "Mortgage APR you expect to be offered."
                    },
                    "termYears": {
                      "type": "integer",
                      "minimum": 1,
                      "maximum": 50,
                      "x-unit": "years",
                      "description": "Loan term in years. Always echoed; 30 when not sent."
                    },
                    "propertyTaxRatePct": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 10,
                      "x-unit": "percent",
                      "description": "Annual property tax as a percent of the home's price. Defaults to the US average effective rate; county rates run from about 0.3% to over 2%, so last year's bill divided by the price is the number to send. Always echoed; 0.89 when not sent."
                    },
                    "insuranceAnnual": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 1000000,
                      "x-unit": "usd",
                      "description": "Your own annual homeowners premium, if you have a quote. Leave it out and the premium is read off the measured curve described in the notes, which re-prices at every home price the search tries. Echoed only when sent."
                    },
                    "pmiRatePct": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 5,
                      "x-unit": "percent",
                      "description": "Annual PMI as a percent of the loan, charged only where the answer lands above 80% loan-to-value. Defaults to 0.5%, mid-range for the $30–$70 a month per $100,000 borrowed Freddie Mac describes. Send `pmiRatePct=0` for a payment with no mortgage insurance in it — the right call for a VA loan, or where you are solving the lender's PITI rather than what underwriting measures. Always echoed; 0.5 when not sent."
                    }
                  },
                  "required": [
                    "income",
                    "monthlyDebts",
                    "downPayment",
                    "rate",
                    "termYears",
                    "propertyTaxRatePct",
                    "pmiRatePct"
                  ]
                },
                "result": {
                  "type": "object",
                  "description": "The answer.",
                  "properties": {
                    "maxPrice": {
                      "description": "The highest home price whose full payment fits both ratios, under the 28/36 rule."
                    },
                    "loanAmount": {
                      "description": "What has to be borrowed at that price."
                    },
                    "downPaymentPercent": {
                      "description": "The down payment as a percent of that price."
                    },
                    "incomeMultiple": {
                      "description": "The price as a multiple of gross annual income — the figure the “3 to 4 times income” rule of thumb is about."
                    },
                    "monthlyBudget": {
                      "description": "The housing budget that binds: the lower of the two below."
                    },
                    "frontEndBudget": {
                      "description": "Housing alone, under the front-end ratio."
                    },
                    "backEndBudget": {
                      "description": "What is left for housing under the back-end ratio, after the other debts."
                    },
                    "limitedBy": {
                      "description": "\"income\" when the front-end ratio is the ceiling, \"debts\" when the other payments are — the one field that says what to do about the answer.",
                      "type": "string",
                      "enum": [
                        "income",
                        "debts"
                      ]
                    },
                    "principalAndInterest": {
                      "description": "The mortgage payment itself at that price."
                    },
                    "monthlyTax": {
                      "description": "Estimated property tax per month at that price."
                    },
                    "monthlyInsurance": {
                      "description": "Estimated homeowners insurance per month at that price."
                    },
                    "monthlyPmi": {
                      "description": "Mortgage insurance per month at that price, charged above 80% loan-to-value. 0 when `pmiApplies` is false or `pmiRatePct=0` was sent."
                    },
                    "totalMonthlyPayment": {
                      "description": "Principal, interest, tax, insurance and mortgage insurance — the whole housing payment, which is the figure the ratios are measured against (Fannie Mae's PITIA)."
                    },
                    "pmiApplies": {
                      "description": "True when the down payment lands under 20% of the price, where a conventional loan adds the PMI that is charged in `monthlyPmi` and in the payment above."
                    },
                    "byBenchmark": {
                      "description": "The same solve at each underwriting benchmark — the 28/36 convention and FHA's two manual-underwriting pairs — each with its ratios, its price, its payment and the authority behind it. Strictest first."
                    }
                  }
                },
                "notes": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                },
                "warnings": {
                  "type": "array",
                  "description": "Values that were accepted but may not say what you meant. Percentages here are whole numbers, so 6.5 means 6.5%; a percent parameter sent 0.065 is read as 0.065% and warned about rather than silently rescaled. Absent when there is nothing to say.",
                  "items": {
                    "type": "object",
                    "required": [
                      "param",
                      "code",
                      "message",
                      "sent",
                      "didYouMean"
                    ],
                    "properties": {
                      "param": {
                        "type": "string",
                        "description": "The parameter the warning is about."
                      },
                      "code": {
                        "type": "string",
                        "enum": [
                          "percent_looks_like_fraction"
                        ],
                        "description": "The machine-readable kind. Switch on this rather than on the message."
                      },
                      "message": {
                        "type": "string",
                        "description": "One sentence: what was read, and what to send instead."
                      },
                      "sent": {
                        "type": "number",
                        "description": "The value you sent, as it was parsed."
                      },
                      "didYouMean": {
                        "type": "number",
                        "description": "The value you probably meant — retry with this."
                      }
                    }
                  }
                },
                "meta": {
                  "type": "object",
                  "description": "Provenance: the source site, the matching calculator page, the docs and the disclaimer.",
                  "properties": {
                    "source": {
                      "type": "string"
                    },
                    "calculator": {
                      "type": "string"
                    },
                    "docs": {
                      "type": "string"
                    },
                    "disclaimer": {
                      "type": "string"
                    }
                  }
                }
              },
              "required": [
                "endpoint",
                "inputs",
                "result",
                "meta"
              ]
            }
          }
        }
      },
      "rent-affordability-result": {
        "description": "The how much rent can i afford answer.",
        "headers": {
          "Cache-Control": {
            "description": "Long-lived: the same URL is the same answer forever.",
            "schema": {
              "type": "string"
            }
          },
          "X-RateLimit-Remaining": {
            "description": "Requests left in the current window.",
            "schema": {
              "type": "integer"
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "properties": {
                "endpoint": {
                  "type": "string",
                  "const": "rent-affordability"
                },
                "inputs": {
                  "type": "object",
                  "description": "The parsed inputs the answer was computed from: your request with defaults filled in, money parsed out of strings like \"$400,000\", and enum spellings canonicalised.",
                  "properties": {
                    "income": {
                      "type": "number",
                      "minimum": 1,
                      "maximum": 100000000,
                      "x-unit": "usd",
                      "description": "Gross household income per year, before tax. Annual, not monthly. Gross rather than take-home, because every rule here is written on gross — the landlord's 3x test included."
                    },
                    "monthlyDebts": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 1000000,
                      "x-unit": "usd",
                      "description": "Required monthly payments on everything that is not housing — car, student loans, credit-card minimums, alimony and child support. Groceries, phone and utilities are not debt; utilities have their own parameter below. Always echoed; 0 when not sent."
                    },
                    "monthlyUtilities": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 100000,
                      "x-unit": "usd",
                      "description": "Electric, gas, water and trash you pay on top of the rent. HUD counts these inside housing cost, so they move `costBurdenRent` and `burdenAtMaxRent` — send them or those two answers are optimistic. Always echoed; 0 when not sent."
                    },
                    "sharePct": {
                      "type": "number",
                      "minimum": 1,
                      "maximum": 100,
                      "x-unit": "percent",
                      "description": "Share of gross income to target for rent. Defaults to the 30% convention, which is a rule of thumb nobody set — see the notes. Always echoed; 30 when not sent."
                    },
                    "backEndPct": {
                      "type": "number",
                      "minimum": 1,
                      "maximum": 100,
                      "x-unit": "percent",
                      "description": "Back-end debt-to-income ratio used as the second ceiling on rent plus all other debt. Defaults to 36, the back-end half of the 28/36 lending convention. `byBenchmark` answers at every benchmark in the table regardless of what is sent here. Always echoed; 36 when not sent."
                    },
                    "dependents": {
                      "type": "integer",
                      "minimum": 0,
                      "maximum": 20,
                      "description": "Number of dependents. Used only by `federalFormula`, where each one is a deduction under 24 CFR § 5.611(a)(1). It does not change `maxRent` — no budgeting rule of thumb accounts for household size. Always echoed; 0 when not sent."
                    },
                    "childcareAnnual": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 1000000,
                      "x-unit": "usd",
                      "description": "Reasonable childcare per year needed to work or study. Deductible in full under 24 CFR § 5.611(a)(4), so it enters `federalFormula` only. Always echoed; 0 when not sent."
                    },
                    "elderlyOrDisabled": {
                      "type": "boolean",
                      "description": "True for an elderly or disabled family, which unlocks the § 5.611(a)(2) deduction and the medical deduction below. `federalFormula` only. Always echoed; false when not sent."
                    },
                    "medicalAnnual": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 1000000,
                      "x-unit": "usd",
                      "description": "Unreimbursed health, medical and attendant-care costs per year. Deductible only for an elderly or disabled family, and only above 10 percent of annual income. `federalFormula` only. Always echoed; 0 when not sent."
                    }
                  },
                  "required": [
                    "income",
                    "monthlyDebts",
                    "monthlyUtilities",
                    "sharePct",
                    "backEndPct",
                    "dependents",
                    "childcareAnnual",
                    "elderlyOrDisabled",
                    "medicalAnnual"
                  ]
                },
                "result": {
                  "type": "object",
                  "description": "The answer.",
                  "properties": {
                    "maxRent": {
                      "description": "The answer: the lower of the share rule and the back-end ratio, never negative."
                    },
                    "limitedBy": {
                      "description": "\"share\" when the percent-of-income rule is the ceiling, \"debts\" when the other payments are — the field that says what to do about the answer.",
                      "type": "string",
                      "enum": [
                        "share",
                        "debts"
                      ]
                    },
                    "grossMonthlyIncome": {
                      "description": "Gross annual income ÷ 12, which every ratio here divides by."
                    },
                    "shareOfIncome": {
                      "description": "`sharePct` of gross monthly income — the rule as it is usually applied."
                    },
                    "debtAdjusted": {
                      "description": "Rent the back-end ratio leaves room for once the other debts are paid, floored at zero."
                    },
                    "costBurdenRent": {
                      "description": "The rent at which this household crosses HUD's cost-burden line — 30% of gross **less** the utility bill, because utilities sit inside HUD's numerator."
                    },
                    "severeCostBurdenRent": {
                      "description": "The same at HUD's severe line, 50% of gross less utilities."
                    },
                    "burdenAtMaxRent": {
                      "description": "\"not-burdened\", \"cost-burdened\" or \"severely-cost-burdened\" — where `maxRent` plus utilities falls under 24 CFR § 91.5.",
                      "type": "string",
                      "enum": [
                        "not-burdened",
                        "cost-burdened",
                        "severely-cost-burdened"
                      ]
                    },
                    "landlordMaxRent": {
                      "description": "Gross monthly income ÷ 3 — the most rent a landlord applying the usual \"income must be at least 3x the rent\" screen would accept."
                    },
                    "leftOver": {
                      "description": "Gross monthly income less rent, utilities and other debt."
                    },
                    "byBenchmark": {
                      "description": "The rent left over at each real underwriting back-end ratio — the 28/36 convention's 36 and FHA's 43 and 47 — each with the authority behind it. Strictest first."
                    },
                    "federalFormula": {
                      "description": "What 24 CFR § 5.628 would charge this household if it were assisted: 30% of monthly **adjusted** income or 10% of monthly gross, whichever is higher, with the § 5.611 deductions applied. A different answer to the same 30%."
                    },
                    "thresholds": {
                      "description": "HUD's two cost-burden thresholds and what they measure, verbatim from 24 CFR § 91.5."
                    }
                  }
                },
                "notes": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                },
                "warnings": {
                  "type": "array",
                  "description": "Values that were accepted but may not say what you meant. Percentages here are whole numbers, so 6.5 means 6.5%; a percent parameter sent 0.065 is read as 0.065% and warned about rather than silently rescaled. Absent when there is nothing to say.",
                  "items": {
                    "type": "object",
                    "required": [
                      "param",
                      "code",
                      "message",
                      "sent",
                      "didYouMean"
                    ],
                    "properties": {
                      "param": {
                        "type": "string",
                        "description": "The parameter the warning is about."
                      },
                      "code": {
                        "type": "string",
                        "enum": [
                          "percent_looks_like_fraction"
                        ],
                        "description": "The machine-readable kind. Switch on this rather than on the message."
                      },
                      "message": {
                        "type": "string",
                        "description": "One sentence: what was read, and what to send instead."
                      },
                      "sent": {
                        "type": "number",
                        "description": "The value you sent, as it was parsed."
                      },
                      "didYouMean": {
                        "type": "number",
                        "description": "The value you probably meant — retry with this."
                      }
                    }
                  }
                },
                "meta": {
                  "type": "object",
                  "description": "Provenance: the source site, the matching calculator page, the docs and the disclaimer.",
                  "properties": {
                    "source": {
                      "type": "string"
                    },
                    "calculator": {
                      "type": "string"
                    },
                    "docs": {
                      "type": "string"
                    },
                    "disclaimer": {
                      "type": "string"
                    }
                  }
                }
              },
              "required": [
                "endpoint",
                "inputs",
                "result",
                "meta"
              ]
            }
          }
        }
      },
      "car-affordability-result": {
        "description": "The how much car can i afford answer.",
        "headers": {
          "Cache-Control": {
            "description": "Long-lived: the same URL is the same answer forever.",
            "schema": {
              "type": "string"
            }
          },
          "X-RateLimit-Remaining": {
            "description": "Requests left in the current window.",
            "schema": {
              "type": "integer"
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "properties": {
                "endpoint": {
                  "type": "string",
                  "const": "car-affordability"
                },
                "inputs": {
                  "type": "object",
                  "description": "The parsed inputs the answer was computed from: your request with defaults filled in, money parsed out of strings like \"$400,000\", and enum spellings canonicalised.",
                  "properties": {
                    "takeHome": {
                      "type": "number",
                      "minimum": 1,
                      "maximum": 10000000,
                      "x-unit": "usd",
                      "description": "Monthly pay after tax. Take-home rather than gross, because the rule is written on take-home — sending gross overstates every answer here by roughly a quarter."
                    },
                    "downPayment": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 10000000,
                      "x-unit": "usd",
                      "description": "Down payment plus trade-in. It adds to the price directly and is what the rule's 20% leg is measured against. Always echoed; 0 when not sent."
                    },
                    "rate": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 100,
                      "x-unit": "percent",
                      "description": "Annual percentage rate on the car loan."
                    },
                    "termMonths": {
                      "type": "integer",
                      "minimum": 1,
                      "maximum": 120,
                      "x-unit": "months",
                      "description": "Loan term in months. Defaults to the rule's 48 — `byTerm` prices 36, 48, 60 and 72 at the same monthly payment regardless, which is the cheapest way to see what stretching costs. Always echoed; 48 when not sent."
                    },
                    "sharePct": {
                      "type": "number",
                      "minimum": 1,
                      "maximum": 100,
                      "x-unit": "percent",
                      "description": "Share of take-home pay for **all** car costs, payment and running costs together. Defaults to the rule's 10, which is a convention nobody set — see the notes. Always echoed; 10 when not sent."
                    },
                    "area": {
                      "type": "string",
                      "enum": [
                        "northeast",
                        "boston",
                        "new-york",
                        "philadelphia",
                        "midwest",
                        "chicago",
                        "cleveland",
                        "detroit",
                        "minneapolis-st-paul",
                        "st-louis",
                        "south",
                        "atlanta",
                        "baltimore",
                        "dallas-ft-worth",
                        "houston",
                        "miami",
                        "tampa",
                        "washington-dc",
                        "west",
                        "anchorage",
                        "denver",
                        "honolulu",
                        "los-angeles",
                        "phoenix",
                        "san-diego",
                        "san-francisco",
                        "seattle"
                      ],
                      "description": "Where the car is run, as an IRS area id (\"miami\", \"west\"), the label the IRS itself prints (\"Washington, D.C.\", \"West region\") or a USPS state code (\"tx\" or \"TX\", either case, which resolves to its census region). Picks the IRS operating allowance used for insurance, fuel and upkeep. Omit it and the four regions' mean is used instead, flagged as `national-average`. Echoed only when sent."
                    },
                    "monthlyOperatingCosts": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 100000,
                      "description": "What insurance, fuel and upkeep actually cost you each month. Overrides `area` when sent, and it is the number to send if you know it — the IRS allowance is a ceiling on what a taxpayer may claim, not a measurement of your bills. Echoed only when sent."
                    },
                    "cars": {
                      "type": "integer",
                      "minimum": 1,
                      "maximum": 2,
                      "description": "How many cars the household runs. Scales both IRS allowances; the table itself only goes to two. Always echoed; 1 when not sent."
                    }
                  },
                  "required": [
                    "takeHome",
                    "downPayment",
                    "rate",
                    "termMonths",
                    "sharePct",
                    "cars"
                  ]
                },
                "result": {
                  "type": "object",
                  "description": "The answer.",
                  "properties": {
                    "maxPrice": {
                      "description": "The answer: what `maxPayment` finances over the term, plus the down payment."
                    },
                    "maxPayment": {
                      "description": "What is left of the car budget for the loan payment once running costs are paid — the figure a payment-only calculator would have handed you as the whole ten percent."
                    },
                    "totalCarBudget": {
                      "description": "`sharePct` of take-home pay: the whole car allowance, payment included."
                    },
                    "operatingCosts": {
                      "description": "Monthly insurance, fuel and upkeep used in the answer."
                    },
                    "operatingCostBasis": {
                      "description": "\"provided\" when you sent a figure, \"irs-area\" when it came from the IRS table for `area`, \"national-average\" when it is the unweighted mean of the four census regions — which is this API's arithmetic, not an IRS figure.",
                      "type": "string",
                      "enum": [
                        "provided",
                        "irs-area",
                        "national-average"
                      ]
                    },
                    "operatingCostArea": {
                      "description": "The IRS area the allowance came from, or null."
                    },
                    "budgetCoversOperatingCosts": {
                      "description": "False when running costs alone exhaust the allowance. Not an error: at that income the rule leaves nothing for a payment, however long the loan."
                    },
                    "takeHomeForAnyPayment": {
                      "description": "The monthly take-home pay at which `sharePct` first covers the running costs — below it, `maxPayment` is zero."
                    },
                    "maxLoan": {
                      "description": "The loan `maxPayment` supports over `termMonths` at `rate`."
                    },
                    "totalInterest": {
                      "description": "Interest paid over the full term at that payment."
                    },
                    "meetsFourYearTerm": {
                      "description": "False past the rule's second leg, four years."
                    },
                    "operatingCostAreaLabel": {
                      "description": "The area's label as the IRS table prints it (\"Miami\", \"West region\"), or null."
                    },
                    "termMonths": {
                      "description": "The term the headline answer was solved over, in months."
                    },
                    "twentyPctDown": {
                      "description": "The rule's first leg, measured rather than enforced: whether the down payment reaches a fifth of `maxPrice`, the priciest car a fifth of it would cover, and the shortfall."
                    },
                    "byTerm": {
                      "description": "The same monthly payment solved over 36, 48, 60 and 72 months, with the interest each costs and whether it is inside the rule."
                    },
                    "irsAllowance": {
                      "description": "What the IRS treats as a necessary monthly expense for the same car — the ownership (loan or lease) allowance plus the operating allowance — and what that is as a share of your take-home pay. Usually well above what the rule of thumb allows."
                    },
                    "rule": {
                      "description": "The 20/4/10 convention's three legs, and the statement that no agency sets it."
                    }
                  }
                },
                "notes": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                },
                "warnings": {
                  "type": "array",
                  "description": "Values that were accepted but may not say what you meant. Percentages here are whole numbers, so 6.5 means 6.5%; a percent parameter sent 0.065 is read as 0.065% and warned about rather than silently rescaled. Absent when there is nothing to say.",
                  "items": {
                    "type": "object",
                    "required": [
                      "param",
                      "code",
                      "message",
                      "sent",
                      "didYouMean"
                    ],
                    "properties": {
                      "param": {
                        "type": "string",
                        "description": "The parameter the warning is about."
                      },
                      "code": {
                        "type": "string",
                        "enum": [
                          "percent_looks_like_fraction"
                        ],
                        "description": "The machine-readable kind. Switch on this rather than on the message."
                      },
                      "message": {
                        "type": "string",
                        "description": "One sentence: what was read, and what to send instead."
                      },
                      "sent": {
                        "type": "number",
                        "description": "The value you sent, as it was parsed."
                      },
                      "didYouMean": {
                        "type": "number",
                        "description": "The value you probably meant — retry with this."
                      }
                    }
                  }
                },
                "meta": {
                  "type": "object",
                  "description": "Provenance: the source site, the matching calculator page, the docs, the disclaimer, and dataVintage — the dated editions this answer depends on.",
                  "properties": {
                    "source": {
                      "type": "string"
                    },
                    "calculator": {
                      "type": "string"
                    },
                    "docs": {
                      "type": "string"
                    },
                    "disclaimer": {
                      "type": "string"
                    },
                    "dataVintage": {
                      "type": "array",
                      "description": "The dated editions behind this answer — figures a publisher set or measured for a named year. Present only where the answer depends on one; arithmetic endpoints omit it.",
                      "items": {
                        "type": "object",
                        "required": [
                          "id",
                          "publisher",
                          "edition",
                          "dataYear",
                          "cycleYears",
                          "supersededFrom",
                          "source"
                        ],
                        "properties": {
                          "id": {
                            "type": "string",
                            "description": "Stable identifier for the edition, matching DATA_VINTAGES in @calcwise/finance."
                          },
                          "publisher": {
                            "type": "string",
                            "description": "The body that measured or set these figures."
                          },
                          "edition": {
                            "type": "string",
                            "description": "The specific publication the figures were transcribed from."
                          },
                          "dataYear": {
                            "type": "integer",
                            "description": "The year the figures describe, or are in force for."
                          },
                          "cycleYears": {
                            "type": "integer",
                            "description": "Years between editions: 1 for an annual series, 3 for the triennial SCF."
                          },
                          "supersededFrom": {
                            "type": "string",
                            "format": "date",
                            "description": "The ISO date (UTC) from which a newer edition is reliably published and this one is a year behind. Compare it against your own clock — this response is cacheable, so it carries no live verdict about its own freshness."
                          },
                          "source": {
                            "type": "string",
                            "description": "The page or notice the current figures were read from."
                          }
                        }
                      }
                    }
                  }
                }
              },
              "required": [
                "endpoint",
                "inputs",
                "result",
                "meta"
              ]
            }
          }
        }
      },
      "pmi-result": {
        "description": "The pmi termination schedule answer.",
        "headers": {
          "Cache-Control": {
            "description": "Long-lived: the same URL is the same answer forever.",
            "schema": {
              "type": "string"
            }
          },
          "X-RateLimit-Remaining": {
            "description": "Requests left in the current window.",
            "schema": {
              "type": "integer"
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "properties": {
                "endpoint": {
                  "type": "string",
                  "const": "pmi"
                },
                "inputs": {
                  "type": "object",
                  "description": "The parsed inputs the answer was computed from: your request with defaults filled in, money parsed out of strings like \"$400,000\", and enum spellings canonicalised.",
                  "properties": {
                    "principal": {
                      "type": "number",
                      "minimum": 1,
                      "maximum": 10000000000,
                      "x-unit": "usd",
                      "description": "Original loan amount."
                    },
                    "rate": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 100,
                      "x-unit": "percent",
                      "description": "Annual interest rate (APR)."
                    },
                    "termMonths": {
                      "type": "integer",
                      "minimum": 1,
                      "maximum": 1200,
                      "x-unit": "months",
                      "description": "Loan term in months."
                    },
                    "originalValue": {
                      "type": "number",
                      "minimum": 1,
                      "maximum": 10000000000,
                      "x-unit": "usd",
                      "description": "The home's original value — the lower of purchase price and original appraised value, which is the figure the rule is measured against."
                    },
                    "pmiMonthly": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 100000,
                      "x-unit": "usd",
                      "description": "PMI premium per month."
                    },
                    "extraMonthly": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 10000000,
                      "x-unit": "usd",
                      "description": "Extra principal per month, which brings the automatic date forward. Always echoed; 0 when not sent."
                    }
                  },
                  "required": [
                    "principal",
                    "rate",
                    "termMonths",
                    "originalValue",
                    "pmiMonthly",
                    "extraMonthly"
                  ]
                },
                "result": {
                  "type": "object",
                  "description": "The answer.",
                  "properties": {
                    "automaticEndMonth": {
                      "description": "Month PMI must be dropped without you asking."
                    },
                    "automaticEndYear": {
                      "description": "That month expressed in years."
                    },
                    "automaticEndLabel": {
                      "description": "That month in words, e.g. \"11 yr 3 mo\"."
                    },
                    "requestableFromMonth": {
                      "description": "First month you may request cancellation (80% of original value)."
                    },
                    "endsAtAmortizationMidpoint": {
                      "description": "True when the midpoint backstop, not the 78% balance, is what ends it."
                    },
                    "totalIfAutomatic": {
                      "description": "Total PMI paid if you wait for automatic termination."
                    },
                    "totalIfRequested": {
                      "description": "Total PMI paid if you request cancellation the first month you can."
                    },
                    "savingsFromRequesting": {
                      "description": "The difference — what asking is worth."
                    },
                    "pmi": {
                      "description": "Present, and null, only when no PMI is charged at all — 20% or more down, so there is nothing to cancel. Every other field is then absent."
                    },
                    "reason": {
                      "description": "Why there is no schedule, on that same no-PMI answer."
                    }
                  }
                },
                "notes": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                },
                "warnings": {
                  "type": "array",
                  "description": "Values that were accepted but may not say what you meant. Percentages here are whole numbers, so 6.5 means 6.5%; a percent parameter sent 0.065 is read as 0.065% and warned about rather than silently rescaled. Absent when there is nothing to say.",
                  "items": {
                    "type": "object",
                    "required": [
                      "param",
                      "code",
                      "message",
                      "sent",
                      "didYouMean"
                    ],
                    "properties": {
                      "param": {
                        "type": "string",
                        "description": "The parameter the warning is about."
                      },
                      "code": {
                        "type": "string",
                        "enum": [
                          "percent_looks_like_fraction"
                        ],
                        "description": "The machine-readable kind. Switch on this rather than on the message."
                      },
                      "message": {
                        "type": "string",
                        "description": "One sentence: what was read, and what to send instead."
                      },
                      "sent": {
                        "type": "number",
                        "description": "The value you sent, as it was parsed."
                      },
                      "didYouMean": {
                        "type": "number",
                        "description": "The value you probably meant — retry with this."
                      }
                    }
                  }
                },
                "meta": {
                  "type": "object",
                  "description": "Provenance: the source site, the matching calculator page, the docs and the disclaimer.",
                  "properties": {
                    "source": {
                      "type": "string"
                    },
                    "calculator": {
                      "type": "string"
                    },
                    "docs": {
                      "type": "string"
                    },
                    "disclaimer": {
                      "type": "string"
                    }
                  }
                }
              },
              "required": [
                "endpoint",
                "inputs",
                "result",
                "meta"
              ]
            }
          }
        }
      },
      "debt-to-income-result": {
        "description": "The debt-to-income ratio answer.",
        "headers": {
          "Cache-Control": {
            "description": "Long-lived: the same URL is the same answer forever.",
            "schema": {
              "type": "string"
            }
          },
          "X-RateLimit-Remaining": {
            "description": "Requests left in the current window.",
            "schema": {
              "type": "integer"
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "properties": {
                "endpoint": {
                  "type": "string",
                  "const": "debt-to-income"
                },
                "inputs": {
                  "type": "object",
                  "description": "The parsed inputs the answer was computed from: your request with defaults filled in, money parsed out of strings like \"$400,000\", and enum spellings canonicalised.",
                  "properties": {
                    "monthlyIncome": {
                      "type": "number",
                      "minimum": 1,
                      "maximum": 10000000,
                      "x-unit": "usd",
                      "description": "Gross monthly income, before tax and deductions. Monthly, not annual — /api/v1/pay converts a salary if you have the yearly figure."
                    },
                    "housing": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 1000000,
                      "x-unit": "usd",
                      "description": "Rent, or the mortgage payment including property tax, insurance, HOA dues and mortgage insurance. This is the front-end ratio on its own."
                    },
                    "autoLoans": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 1000000,
                      "x-unit": "usd",
                      "description": "Car and other vehicle payments, per month. Always echoed; 0 when not sent."
                    },
                    "studentLoans": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 1000000,
                      "x-unit": "usd",
                      "description": "Student loan payments, per month. Always echoed; 0 when not sent."
                    },
                    "creditCardMinimums": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 1000000,
                      "x-unit": "usd",
                      "description": "Minimum payments due, not balances — the ratio is built from payments. Always echoed; 0 when not sent."
                    },
                    "alimonyChildSupport": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 1000000,
                      "x-unit": "usd",
                      "description": "Alimony and child support. Counted as debt by 12 CFR 1026.43(c)(7)(i)(A), and the item most often left out. Always echoed; 0 when not sent."
                    },
                    "otherDebt": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 1000000,
                      "x-unit": "usd",
                      "description": "Personal loans and any other recurring debt obligation. Always echoed; 0 when not sent."
                    }
                  },
                  "required": [
                    "monthlyIncome",
                    "housing",
                    "autoLoans",
                    "studentLoans",
                    "creditCardMinimums",
                    "alimonyChildSupport",
                    "otherDebt"
                  ]
                },
                "result": {
                  "type": "object",
                  "description": "The answer.",
                  "properties": {
                    "backEnd": {
                      "description": "All debt as a percent of gross monthly income — \"your DTI\"."
                    },
                    "frontEnd": {
                      "description": "Housing payment alone, as a percent of gross monthly income."
                    },
                    "totalMonthlyDebt": {
                      "description": "Housing plus every non-housing debt payment."
                    },
                    "nonHousingDebt": {
                      "description": "The non-housing part on its own."
                    },
                    "residualIncome": {
                      "description": "Gross monthly income minus total debt. Negative when debt exceeds income, because that is the case worth seeing."
                    },
                    "strictestCleared": {
                      "description": "The id of the strictest benchmark both ratios clear, or null if none do.",
                      "type": "string",
                      "enum": [
                        "conventional-28-36",
                        "fha-manual-31-43",
                        "fha-manual-37-47"
                      ]
                    },
                    "benchmarks": {
                      "description": "Each benchmark with `clearsFrontEnd`, `clearsBackEnd` and `clears`, plus the source it comes from. Strictest first."
                    },
                    "qualifiedMortgage": {
                      "description": "What replaced the 43% limit, as data rather than prose: `thresholdYear`, the six `tiers` in force for it (each with its `lien`, its loan-amount band as numbers and as a sentence, and the `aprSpread` over the average prime offer rate **at or above which** the loan stops being a General QM), the `formerDtiLimit` with the date it stopped applying, and the `citation`. Constant — it is the regulation, not an answer about your inputs."
                    }
                  }
                },
                "notes": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                },
                "meta": {
                  "type": "object",
                  "description": "Provenance: the source site, the matching calculator page, the docs, the disclaimer, and dataVintage — the dated editions this answer depends on.",
                  "properties": {
                    "source": {
                      "type": "string"
                    },
                    "calculator": {
                      "type": "string"
                    },
                    "docs": {
                      "type": "string"
                    },
                    "disclaimer": {
                      "type": "string"
                    },
                    "dataVintage": {
                      "type": "array",
                      "description": "The dated editions behind this answer — figures a publisher set or measured for a named year. Present only where the answer depends on one; arithmetic endpoints omit it.",
                      "items": {
                        "type": "object",
                        "required": [
                          "id",
                          "publisher",
                          "edition",
                          "dataYear",
                          "cycleYears",
                          "supersededFrom",
                          "source"
                        ],
                        "properties": {
                          "id": {
                            "type": "string",
                            "description": "Stable identifier for the edition, matching DATA_VINTAGES in @calcwise/finance."
                          },
                          "publisher": {
                            "type": "string",
                            "description": "The body that measured or set these figures."
                          },
                          "edition": {
                            "type": "string",
                            "description": "The specific publication the figures were transcribed from."
                          },
                          "dataYear": {
                            "type": "integer",
                            "description": "The year the figures describe, or are in force for."
                          },
                          "cycleYears": {
                            "type": "integer",
                            "description": "Years between editions: 1 for an annual series, 3 for the triennial SCF."
                          },
                          "supersededFrom": {
                            "type": "string",
                            "format": "date",
                            "description": "The ISO date (UTC) from which a newer edition is reliably published and this one is a year behind. Compare it against your own clock — this response is cacheable, so it carries no live verdict about its own freshness."
                          },
                          "source": {
                            "type": "string",
                            "description": "The page or notice the current figures were read from."
                          }
                        }
                      }
                    }
                  }
                }
              },
              "required": [
                "endpoint",
                "inputs",
                "result",
                "meta"
              ]
            }
          }
        }
      },
      "credit-card-payoff-result": {
        "description": "The credit card payoff answer.",
        "headers": {
          "Cache-Control": {
            "description": "Long-lived: the same URL is the same answer forever.",
            "schema": {
              "type": "string"
            }
          },
          "X-RateLimit-Remaining": {
            "description": "Requests left in the current window.",
            "schema": {
              "type": "integer"
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "properties": {
                "endpoint": {
                  "type": "string",
                  "const": "credit-card-payoff"
                },
                "inputs": {
                  "type": "object",
                  "description": "The parsed inputs the answer was computed from: your request with defaults filled in, money parsed out of strings like \"$400,000\", and enum spellings canonicalised.",
                  "properties": {
                    "balance": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 1000000000,
                      "x-unit": "usd",
                      "description": "Current balance."
                    },
                    "apr": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 100,
                      "x-unit": "percent",
                      "description": "Annual percentage rate."
                    },
                    "monthlyPayment": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 1000000000,
                      "x-unit": "usd",
                      "description": "Fixed amount paid each month. Ignored when minimumOnly=true. Always echoed; 0 when not sent."
                    },
                    "minimumOnly": {
                      "type": "boolean",
                      "description": "Pay only the card's minimum each month (1% of the balance plus interest, floor $25) instead of a fixed amount. Always echoed; false when not sent."
                    }
                  },
                  "required": [
                    "balance",
                    "apr",
                    "monthlyPayment",
                    "minimumOnly"
                  ]
                },
                "result": {
                  "type": "object",
                  "description": "The answer.",
                  "properties": {
                    "months": {
                      "description": "Months to clear the balance. Null when the payment never clears it."
                    },
                    "termLabel": {
                      "description": "That time in words."
                    },
                    "totalInterest": {
                      "description": "Interest paid getting there."
                    },
                    "totalPaid": {
                      "description": "Balance plus interest."
                    },
                    "firstPayment": {
                      "description": "The first month's payment — the useful number under minimumOnly."
                    },
                    "neverPaysOff": {
                      "description": "True when the payment is at or below the monthly interest."
                    }
                  }
                },
                "notes": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                },
                "warnings": {
                  "type": "array",
                  "description": "Values that were accepted but may not say what you meant. Percentages here are whole numbers, so 6.5 means 6.5%; a percent parameter sent 0.065 is read as 0.065% and warned about rather than silently rescaled. Absent when there is nothing to say.",
                  "items": {
                    "type": "object",
                    "required": [
                      "param",
                      "code",
                      "message",
                      "sent",
                      "didYouMean"
                    ],
                    "properties": {
                      "param": {
                        "type": "string",
                        "description": "The parameter the warning is about."
                      },
                      "code": {
                        "type": "string",
                        "enum": [
                          "percent_looks_like_fraction"
                        ],
                        "description": "The machine-readable kind. Switch on this rather than on the message."
                      },
                      "message": {
                        "type": "string",
                        "description": "One sentence: what was read, and what to send instead."
                      },
                      "sent": {
                        "type": "number",
                        "description": "The value you sent, as it was parsed."
                      },
                      "didYouMean": {
                        "type": "number",
                        "description": "The value you probably meant — retry with this."
                      }
                    }
                  }
                },
                "meta": {
                  "type": "object",
                  "description": "Provenance: the source site, the matching calculator page, the docs and the disclaimer.",
                  "properties": {
                    "source": {
                      "type": "string"
                    },
                    "calculator": {
                      "type": "string"
                    },
                    "docs": {
                      "type": "string"
                    },
                    "disclaimer": {
                      "type": "string"
                    }
                  }
                }
              },
              "required": [
                "endpoint",
                "inputs",
                "result",
                "meta"
              ]
            }
          }
        }
      },
      "debt-snowball-result": {
        "description": "The debt snowball vs. avalanche answer.",
        "headers": {
          "Cache-Control": {
            "description": "Long-lived: the same URL is the same answer forever.",
            "schema": {
              "type": "string"
            }
          },
          "X-RateLimit-Remaining": {
            "description": "Requests left in the current window.",
            "schema": {
              "type": "integer"
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "properties": {
                "endpoint": {
                  "type": "string",
                  "const": "debt-snowball"
                },
                "inputs": {
                  "type": "object",
                  "description": "The parsed inputs the answer was computed from: your request with defaults filled in, money parsed out of strings like \"$400,000\", and enum spellings canonicalised.",
                  "properties": {
                    "balance1": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 10000000,
                      "x-unit": "usd",
                      "description": "Balance owed on the first debt. Send them in any order — the payoff queue is worked out for you."
                    },
                    "apr1": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 100,
                      "x-unit": "percent",
                      "description": "Annual percentage rate on debt 1, e.g. 24.99. This is what the avalanche orders by. Always echoed; 0 when not sent."
                    },
                    "minimum1": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 1000000,
                      "x-unit": "usd",
                      "description": "Minimum payment due on debt 1 each month — the payment, not the balance. Always echoed; 0 when not sent."
                    },
                    "balance2": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 10000000,
                      "x-unit": "usd",
                      "description": "Balance owed on debt 2."
                    },
                    "apr2": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 100,
                      "x-unit": "percent",
                      "description": "Annual percentage rate on debt 2, e.g. 24.99. This is what the avalanche orders by. Always echoed; 0 when not sent."
                    },
                    "minimum2": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 1000000,
                      "x-unit": "usd",
                      "description": "Minimum payment due on debt 2 each month — the payment, not the balance. Always echoed; 0 when not sent."
                    },
                    "balance3": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 10000000,
                      "x-unit": "usd",
                      "description": "Balance owed on debt 3. Leave it out or send 0 to skip the slot. Always echoed; 0 when not sent."
                    },
                    "apr3": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 100,
                      "x-unit": "percent",
                      "description": "Annual percentage rate on debt 3, e.g. 24.99. This is what the avalanche orders by. Always echoed; 0 when not sent."
                    },
                    "minimum3": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 1000000,
                      "x-unit": "usd",
                      "description": "Minimum payment due on debt 3 each month — the payment, not the balance. Always echoed; 0 when not sent."
                    },
                    "balance4": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 10000000,
                      "x-unit": "usd",
                      "description": "Balance owed on debt 4. Leave it out or send 0 to skip the slot. Always echoed; 0 when not sent."
                    },
                    "apr4": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 100,
                      "x-unit": "percent",
                      "description": "Annual percentage rate on debt 4, e.g. 24.99. This is what the avalanche orders by. Always echoed; 0 when not sent."
                    },
                    "minimum4": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 1000000,
                      "x-unit": "usd",
                      "description": "Minimum payment due on debt 4 each month — the payment, not the balance. Always echoed; 0 when not sent."
                    },
                    "balance5": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 10000000,
                      "x-unit": "usd",
                      "description": "Balance owed on debt 5. Leave it out or send 0 to skip the slot. Always echoed; 0 when not sent."
                    },
                    "apr5": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 100,
                      "x-unit": "percent",
                      "description": "Annual percentage rate on debt 5, e.g. 24.99. This is what the avalanche orders by. Always echoed; 0 when not sent."
                    },
                    "minimum5": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 1000000,
                      "x-unit": "usd",
                      "description": "Minimum payment due on debt 5 each month — the payment, not the balance. Always echoed; 0 when not sent."
                    },
                    "balance6": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 10000000,
                      "x-unit": "usd",
                      "description": "Balance owed on debt 6. Leave it out or send 0 to skip the slot. Always echoed; 0 when not sent."
                    },
                    "apr6": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 100,
                      "x-unit": "percent",
                      "description": "Annual percentage rate on debt 6, e.g. 24.99. This is what the avalanche orders by. Always echoed; 0 when not sent."
                    },
                    "minimum6": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 1000000,
                      "x-unit": "usd",
                      "description": "Minimum payment due on debt 6 each month — the payment, not the balance. Always echoed; 0 when not sent."
                    },
                    "extra": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 1000000,
                      "x-unit": "usd",
                      "description": "Everything you can pay above the minimums, per month. This single number decides how fast you are free; the method only decides the order. Always echoed; 0 when not sent."
                    }
                  },
                  "required": [
                    "balance1",
                    "apr1",
                    "minimum1",
                    "balance2",
                    "apr2",
                    "minimum2",
                    "balance3",
                    "apr3",
                    "minimum3",
                    "balance4",
                    "apr4",
                    "minimum4",
                    "balance5",
                    "apr5",
                    "minimum5",
                    "balance6",
                    "apr6",
                    "minimum6",
                    "extra"
                  ]
                },
                "result": {
                  "type": "object",
                  "description": "The answer.",
                  "properties": {
                    "debtCount": {
                      "description": "How many slots carried a balance and were simulated."
                    },
                    "startBalance": {
                      "description": "Everything owed today."
                    },
                    "minimums": {
                      "description": "The minimum payments added up."
                    },
                    "monthlyBudget": {
                      "description": "Minimums plus extra — the same under both methods, which is what makes the comparison fair."
                    },
                    "cheaper": {
                      "description": "\"avalanche\", or null when both orders come out identical. Never \"snowball\": highest-rate-first is optimal for total interest."
                    },
                    "interestSaved": {
                      "description": "Interest the cheaper order saves. Zero when the two orders agree."
                    },
                    "monthsSaved": {
                      "description": "Months the cheaper order saves. Often zero even when the interest differs."
                    },
                    "sameOrder": {
                      "description": "True when both methods pick the same queue, so the two plans are one plan."
                    },
                    "avalanche": {
                      "description": "The highest-rate-first plan: months, termLabel, totalInterest, totalPaid, firstDebtGoneMonth, neverPaysOff, and `order` — one row per debt with its rank, payoff month and interest."
                    },
                    "snowball": {
                      "description": "The smallest-balance-first plan, in the same shape."
                    }
                  }
                },
                "notes": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                },
                "warnings": {
                  "type": "array",
                  "description": "Values that were accepted but may not say what you meant. Percentages here are whole numbers, so 6.5 means 6.5%; a percent parameter sent 0.065 is read as 0.065% and warned about rather than silently rescaled. Absent when there is nothing to say.",
                  "items": {
                    "type": "object",
                    "required": [
                      "param",
                      "code",
                      "message",
                      "sent",
                      "didYouMean"
                    ],
                    "properties": {
                      "param": {
                        "type": "string",
                        "description": "The parameter the warning is about."
                      },
                      "code": {
                        "type": "string",
                        "enum": [
                          "percent_looks_like_fraction"
                        ],
                        "description": "The machine-readable kind. Switch on this rather than on the message."
                      },
                      "message": {
                        "type": "string",
                        "description": "One sentence: what was read, and what to send instead."
                      },
                      "sent": {
                        "type": "number",
                        "description": "The value you sent, as it was parsed."
                      },
                      "didYouMean": {
                        "type": "number",
                        "description": "The value you probably meant — retry with this."
                      }
                    }
                  }
                },
                "meta": {
                  "type": "object",
                  "description": "Provenance: the source site, the matching calculator page, the docs and the disclaimer.",
                  "properties": {
                    "source": {
                      "type": "string"
                    },
                    "calculator": {
                      "type": "string"
                    },
                    "docs": {
                      "type": "string"
                    },
                    "disclaimer": {
                      "type": "string"
                    }
                  }
                }
              },
              "required": [
                "endpoint",
                "inputs",
                "result",
                "meta"
              ]
            }
          }
        }
      },
      "compound-interest-result": {
        "description": "The compound interest answer.",
        "headers": {
          "Cache-Control": {
            "description": "Long-lived: the same URL is the same answer forever.",
            "schema": {
              "type": "string"
            }
          },
          "X-RateLimit-Remaining": {
            "description": "Requests left in the current window.",
            "schema": {
              "type": "integer"
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "properties": {
                "endpoint": {
                  "type": "string",
                  "const": "compound-interest"
                },
                "inputs": {
                  "type": "object",
                  "description": "The parsed inputs the answer was computed from: your request with defaults filled in, money parsed out of strings like \"$400,000\", and enum spellings canonicalised.",
                  "properties": {
                    "principal": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 1000000000000,
                      "x-unit": "usd",
                      "description": "Starting balance."
                    },
                    "contribution": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 1000000000,
                      "x-unit": "usd",
                      "description": "Added at the end of every compounding period. Always echoed; 0 when not sent."
                    },
                    "rate": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 100,
                      "x-unit": "percent",
                      "description": "Nominal annual return."
                    },
                    "years": {
                      "type": "integer",
                      "minimum": 0,
                      "maximum": 100,
                      "x-unit": "years",
                      "description": "How long to run it."
                    },
                    "periodsPerYear": {
                      "type": "integer",
                      "minimum": 1,
                      "maximum": 365,
                      "x-unit": "count",
                      "description": "Compounding periods per year. 12 is monthly, 1 is annual. Always echoed; 12 when not sent."
                    },
                    "series": {
                      "type": "boolean",
                      "description": "Include the year-by-year balance series. Always echoed; false when not sent."
                    }
                  },
                  "required": [
                    "principal",
                    "contribution",
                    "rate",
                    "years",
                    "periodsPerYear",
                    "series"
                  ]
                },
                "result": {
                  "type": "object",
                  "description": "The answer.",
                  "properties": {
                    "balance": {
                      "description": "Final balance."
                    },
                    "contributed": {
                      "description": "Principal plus every contribution."
                    },
                    "growth": {
                      "description": "Balance minus contributed — what the compounding did."
                    },
                    "series": {
                      "description": "Present only when series=true: balance, contributed and growth for each year."
                    }
                  }
                },
                "warnings": {
                  "type": "array",
                  "description": "Values that were accepted but may not say what you meant. Percentages here are whole numbers, so 6.5 means 6.5%; a percent parameter sent 0.065 is read as 0.065% and warned about rather than silently rescaled. Absent when there is nothing to say.",
                  "items": {
                    "type": "object",
                    "required": [
                      "param",
                      "code",
                      "message",
                      "sent",
                      "didYouMean"
                    ],
                    "properties": {
                      "param": {
                        "type": "string",
                        "description": "The parameter the warning is about."
                      },
                      "code": {
                        "type": "string",
                        "enum": [
                          "percent_looks_like_fraction"
                        ],
                        "description": "The machine-readable kind. Switch on this rather than on the message."
                      },
                      "message": {
                        "type": "string",
                        "description": "One sentence: what was read, and what to send instead."
                      },
                      "sent": {
                        "type": "number",
                        "description": "The value you sent, as it was parsed."
                      },
                      "didYouMean": {
                        "type": "number",
                        "description": "The value you probably meant — retry with this."
                      }
                    }
                  }
                },
                "meta": {
                  "type": "object",
                  "description": "Provenance: the source site, the matching calculator page, the docs and the disclaimer.",
                  "properties": {
                    "source": {
                      "type": "string"
                    },
                    "calculator": {
                      "type": "string"
                    },
                    "docs": {
                      "type": "string"
                    },
                    "disclaimer": {
                      "type": "string"
                    }
                  }
                }
              },
              "required": [
                "endpoint",
                "inputs",
                "result",
                "meta"
              ]
            }
          }
        }
      },
      "savings-goal-result": {
        "description": "The savings goal answer.",
        "headers": {
          "Cache-Control": {
            "description": "Long-lived: the same URL is the same answer forever.",
            "schema": {
              "type": "string"
            }
          },
          "X-RateLimit-Remaining": {
            "description": "Requests left in the current window.",
            "schema": {
              "type": "integer"
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "properties": {
                "endpoint": {
                  "type": "string",
                  "const": "savings-goal"
                },
                "inputs": {
                  "type": "object",
                  "description": "The parsed inputs the answer was computed from: your request with defaults filled in, money parsed out of strings like \"$400,000\", and enum spellings canonicalised.",
                  "properties": {
                    "goal": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 1000000000000,
                      "x-unit": "usd",
                      "description": "The amount you are aiming at."
                    },
                    "current": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 1000000000000,
                      "x-unit": "usd",
                      "description": "What you have saved already. Always echoed; 0 when not sent."
                    },
                    "monthly": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 1000000000,
                      "x-unit": "usd",
                      "description": "What you put in each month. Drives the time-to-goal answer. Always echoed; 0 when not sent."
                    },
                    "rate": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 100,
                      "x-unit": "percent",
                      "description": "Annual return or APY on the savings. Always echoed; 0 when not sent."
                    },
                    "deadlineYears": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 100,
                      "x-unit": "years",
                      "description": "Optional deadline. When above 0, the response also says what monthly deposit lands on the goal exactly then. Always echoed; 0 when not sent."
                    }
                  },
                  "required": [
                    "goal",
                    "current",
                    "monthly",
                    "rate",
                    "deadlineYears"
                  ]
                },
                "result": {
                  "type": "object",
                  "description": "The answer.",
                  "properties": {
                    "reached": {
                      "description": "Whether `monthly` gets there within 100 years."
                    },
                    "months": {
                      "description": "Months to the goal at `monthly`. Null if it never arrives."
                    },
                    "termLabel": {
                      "description": "That time in words."
                    },
                    "balanceAtGoal": {
                      "description": "Balance the month the goal is met."
                    },
                    "requiredMonthly": {
                      "description": "Present only with deadlineYears > 0: the deposit that lands exactly on the goal by the deadline."
                    }
                  }
                },
                "warnings": {
                  "type": "array",
                  "description": "Values that were accepted but may not say what you meant. Percentages here are whole numbers, so 6.5 means 6.5%; a percent parameter sent 0.065 is read as 0.065% and warned about rather than silently rescaled. Absent when there is nothing to say.",
                  "items": {
                    "type": "object",
                    "required": [
                      "param",
                      "code",
                      "message",
                      "sent",
                      "didYouMean"
                    ],
                    "properties": {
                      "param": {
                        "type": "string",
                        "description": "The parameter the warning is about."
                      },
                      "code": {
                        "type": "string",
                        "enum": [
                          "percent_looks_like_fraction"
                        ],
                        "description": "The machine-readable kind. Switch on this rather than on the message."
                      },
                      "message": {
                        "type": "string",
                        "description": "One sentence: what was read, and what to send instead."
                      },
                      "sent": {
                        "type": "number",
                        "description": "The value you sent, as it was parsed."
                      },
                      "didYouMean": {
                        "type": "number",
                        "description": "The value you probably meant — retry with this."
                      }
                    }
                  }
                },
                "meta": {
                  "type": "object",
                  "description": "Provenance: the source site, the matching calculator page, the docs and the disclaimer.",
                  "properties": {
                    "source": {
                      "type": "string"
                    },
                    "calculator": {
                      "type": "string"
                    },
                    "docs": {
                      "type": "string"
                    },
                    "disclaimer": {
                      "type": "string"
                    }
                  }
                }
              },
              "required": [
                "endpoint",
                "inputs",
                "result",
                "meta"
              ]
            }
          }
        }
      },
      "emergency-fund-result": {
        "description": "The emergency fund answer.",
        "headers": {
          "Cache-Control": {
            "description": "Long-lived: the same URL is the same answer forever.",
            "schema": {
              "type": "string"
            }
          },
          "X-RateLimit-Remaining": {
            "description": "Requests left in the current window.",
            "schema": {
              "type": "integer"
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "properties": {
                "endpoint": {
                  "type": "string",
                  "const": "emergency-fund"
                },
                "inputs": {
                  "type": "object",
                  "description": "The parsed inputs the answer was computed from: your request with defaults filled in, money parsed out of strings like \"$400,000\", and enum spellings canonicalised.",
                  "properties": {
                    "monthlyExpenses": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 1000000,
                      "x-unit": "usd",
                      "description": "Essential monthly outgoings: housing, food, utilities, transport, insurance and minimum debt payments. Essentials, not the whole budget — restaurants, holidays and subscriptions pause in an emergency."
                    },
                    "months": {
                      "type": "number",
                      "minimum": 1,
                      "maximum": 24,
                      "x-unit": "months",
                      "description": "Months of cushion to size the target against. Defaults to 6, which is a convention rather than a rule — see the notes. `ladder` answers for 1, 3, 6 and 12 regardless of what you send. Always echoed; 6 when not sent."
                    },
                    "saved": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 1000000000,
                      "x-unit": "usd",
                      "description": "What is set aside for emergencies today. Always echoed; 0 when not sent."
                    },
                    "monthlySaving": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 1000000,
                      "x-unit": "usd",
                      "description": "What can be added each month. Drives `monthsToTarget`; leave it out and that answer is null. Always echoed; 0 when not sent."
                    },
                    "annualIncome": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 100000000,
                      "x-unit": "usd",
                      "description": "Annual pre-tax family income. Drives `peers.byIncome` only — it changes no arithmetic. Omit it and that cut comes back null. Always echoed; 0 when not sent."
                    },
                    "age": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 120,
                      "x-unit": "years",
                      "description": "Age of the adult. Drives `peers.byAge` only — it changes no arithmetic. The survey covers adults, so an age under 18 falls outside every published band and returns null. Always echoed; 0 when not sent."
                    }
                  },
                  "required": [
                    "monthlyExpenses",
                    "months",
                    "saved",
                    "monthlySaving",
                    "annualIncome",
                    "age"
                  ]
                },
                "result": {
                  "type": "object",
                  "description": "The answer.",
                  "properties": {
                    "target": {
                      "description": "Dollars the chosen cushion comes to — `monthlyExpenses` × `months`."
                    },
                    "targetMonths": {
                      "description": "Months of cushion the target represents, echoed."
                    },
                    "gap": {
                      "description": "Dollars still to save. 0 once the target is met."
                    },
                    "fundedPct": {
                      "description": "Share of the target already saved, 0–100."
                    },
                    "funded": {
                      "description": "Whether the target is met."
                    },
                    "monthsCovered": {
                      "description": "**The target-independent answer:** months of essentials current savings would cover. Does not move when `months` changes. Null when `monthlyExpenses` is 0."
                    },
                    "monthsToTarget": {
                      "description": "Whole months of saving to close the gap, ignoring interest. 0 when already funded; null when nothing is being saved, or when it would take over 100 years."
                    },
                    "termLabel": {
                      "description": "`monthsToTarget` in words, or null."
                    },
                    "coversSmallShock": {
                      "description": "Whether savings alone cover the Federal Reserve's $400 emergency-expense question."
                    },
                    "atRainyDayBenchmark": {
                      "description": "Whether savings reach three months of expenses — the rainy-day cushion the Federal Reserve measures households against."
                    },
                    "ladder": {
                      "description": "The four cushion conventions (1, 3, 6, 12 months) with `amount`, `reached` and `shortfall` against each, plus who each size is usually suggested for."
                    },
                    "benchmarks": {
                      "description": "The measured Federal Reserve figures this endpoint reports against — the $400 and three-month shares, and how many adults can cover neither — with their survey year."
                    },
                    "peers": {
                      "description": "**Is that normal?** — the measured company this household keeps, for whichever cuts were supplied. `byIncome` and `byAge` give the share of adults in that band with three months set aside; `savingsCeiling` places `saved` in the Fed's largest-expense-from-savings distribution with the share below it and at or above it. Each cut is null when its input was not sent, and the full published bands ride along in `bands` so a caller can draw the whole distribution rather than one row."
                    }
                  }
                },
                "notes": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                },
                "meta": {
                  "type": "object",
                  "description": "Provenance: the source site, the matching calculator page, the docs, the disclaimer, and dataVintage — the dated editions this answer depends on.",
                  "properties": {
                    "source": {
                      "type": "string"
                    },
                    "calculator": {
                      "type": "string"
                    },
                    "docs": {
                      "type": "string"
                    },
                    "disclaimer": {
                      "type": "string"
                    },
                    "dataVintage": {
                      "type": "array",
                      "description": "The dated editions behind this answer — figures a publisher set or measured for a named year. Present only where the answer depends on one; arithmetic endpoints omit it.",
                      "items": {
                        "type": "object",
                        "required": [
                          "id",
                          "publisher",
                          "edition",
                          "dataYear",
                          "cycleYears",
                          "supersededFrom",
                          "source"
                        ],
                        "properties": {
                          "id": {
                            "type": "string",
                            "description": "Stable identifier for the edition, matching DATA_VINTAGES in @calcwise/finance."
                          },
                          "publisher": {
                            "type": "string",
                            "description": "The body that measured or set these figures."
                          },
                          "edition": {
                            "type": "string",
                            "description": "The specific publication the figures were transcribed from."
                          },
                          "dataYear": {
                            "type": "integer",
                            "description": "The year the figures describe, or are in force for."
                          },
                          "cycleYears": {
                            "type": "integer",
                            "description": "Years between editions: 1 for an annual series, 3 for the triennial SCF."
                          },
                          "supersededFrom": {
                            "type": "string",
                            "format": "date",
                            "description": "The ISO date (UTC) from which a newer edition is reliably published and this one is a year behind. Compare it against your own clock — this response is cacheable, so it carries no live verdict about its own freshness."
                          },
                          "source": {
                            "type": "string",
                            "description": "The page or notice the current figures were read from."
                          }
                        }
                      }
                    }
                  }
                }
              },
              "required": [
                "endpoint",
                "inputs",
                "result",
                "meta"
              ]
            }
          }
        }
      },
      "net-worth-result": {
        "description": "The net worth answer.",
        "headers": {
          "Cache-Control": {
            "description": "Long-lived: the same URL is the same answer forever.",
            "schema": {
              "type": "string"
            }
          },
          "X-RateLimit-Remaining": {
            "description": "Requests left in the current window.",
            "schema": {
              "type": "integer"
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "properties": {
                "endpoint": {
                  "type": "string",
                  "const": "net-worth"
                },
                "inputs": {
                  "type": "object",
                  "description": "The parsed inputs the answer was computed from: your request with defaults filled in, money parsed out of strings like \"$400,000\", and enum spellings canonicalised.",
                  "properties": {
                    "cash": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 1000000000,
                      "x-unit": "usd",
                      "description": "Cash, checking and savings. Always echoed; 0 when not sent."
                    },
                    "investments": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 1000000000,
                      "x-unit": "usd",
                      "description": "Taxable brokerage holdings. Counted as liquid alongside `cash`. Always echoed; 0 when not sent."
                    },
                    "retirement": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 1000000000,
                      "x-unit": "usd",
                      "description": "401(k), IRA and other retirement accounts. Counted as an asset but **not** as liquid — reaching it early generally costs tax and a penalty. Always echoed; 0 when not sent."
                    },
                    "home": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 1000000000,
                      "x-unit": "usd",
                      "description": "Current market value of the home — what it would sell for today, not its purchase price. Always echoed; 0 when not sent."
                    },
                    "vehicles": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 1000000000,
                      "x-unit": "usd",
                      "description": "Resale value of vehicles. See the notes on why this one normally falls year over year. Always echoed; 0 when not sent."
                    },
                    "otherAssets": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 1000000000,
                      "x-unit": "usd",
                      "description": "Business interests, collectibles, cash value of insurance, anything else owned. Always echoed; 0 when not sent."
                    },
                    "mortgage": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 1000000000,
                      "x-unit": "usd",
                      "description": "Outstanding mortgage principal. Netted against `home` to give `homeEquity`. Always echoed; 0 when not sent."
                    },
                    "autoLoans": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 1000000000,
                      "x-unit": "usd",
                      "description": "Auto loan balances. Always echoed; 0 when not sent."
                    },
                    "studentLoans": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 1000000000,
                      "x-unit": "usd",
                      "description": "Student loan balances. Always echoed; 0 when not sent."
                    },
                    "creditCards": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 1000000000,
                      "x-unit": "usd",
                      "description": "Credit card balances carried. Subtracted from liquid assets to give `liquidNetWorth`. Always echoed; 0 when not sent."
                    },
                    "otherDebts": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 1000000000,
                      "x-unit": "usd",
                      "description": "Personal loans, medical debt, anything else owed. Also treated as short-term debt. Always echoed; 0 when not sent."
                    },
                    "age": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 120,
                      "x-unit": "years",
                      "description": "Age of the household's reference person. Drives `benchmark` only — it changes no arithmetic. Omit it and `benchmark` comes back null. Always echoed; 0 when not sent."
                    }
                  },
                  "required": [
                    "cash",
                    "investments",
                    "retirement",
                    "home",
                    "vehicles",
                    "otherAssets",
                    "mortgage",
                    "autoLoans",
                    "studentLoans",
                    "creditCards",
                    "otherDebts",
                    "age"
                  ]
                },
                "result": {
                  "type": "object",
                  "description": "The answer.",
                  "properties": {
                    "netWorth": {
                      "description": "Total assets minus total liabilities. Negative is common rather than exceptional — see the notes."
                    },
                    "totalAssets": {
                      "description": "Everything owned, summed."
                    },
                    "totalLiabilities": {
                      "description": "Everything owed, summed."
                    },
                    "negative": {
                      "description": "Whether debts outweigh assets."
                    },
                    "liquidAssets": {
                      "description": "`cash` + `investments` — what could be reached without selling a house or a car."
                    },
                    "liquidNetWorth": {
                      "description": "Liquid assets less short-term debt (`creditCards` + `otherDebts`). **The honest emergency figure**: a brokerage balance offset by a card balance is not really available."
                    },
                    "homeEquity": {
                      "description": "`home` − `mortgage`. Negative when the mortgage exceeds the home's value."
                    },
                    "underwater": {
                      "description": "Whether the mortgage exceeds the home's value. False when there is no home."
                    },
                    "debtToAssetPct": {
                      "description": "Liabilities as a percent of assets. Null when there are no assets to divide by."
                    },
                    "liquidSharePct": {
                      "description": "Share of assets that is liquid, 0–100. Null when there are no assets."
                    },
                    "assetMix": {
                      "description": "Each non-empty asset category with its `amount`, `sharePct` and whether it is `liquid`, in declared order rather than sorted by size."
                    },
                    "benchmark": {
                      "description": "Where this net worth sits against the Federal Reserve's 2022 Survey of Consumer Finances for the household's age band: the band's `median` and `mean`, the `meanToMedianRatio` that explains the gap between them, `vsMedianPct` and `atOrAboveMedian`. Null when no `age` was sent."
                    },
                    "allFamilies": {
                      "description": "Median and mean net worth across all US families in the same survey, with the same ratio — the denominator for \"compared to everyone, not just my age\"."
                    }
                  }
                },
                "notes": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                },
                "meta": {
                  "type": "object",
                  "description": "Provenance: the source site, the matching calculator page, the docs, the disclaimer, and dataVintage — the dated editions this answer depends on.",
                  "properties": {
                    "source": {
                      "type": "string"
                    },
                    "calculator": {
                      "type": "string"
                    },
                    "docs": {
                      "type": "string"
                    },
                    "disclaimer": {
                      "type": "string"
                    },
                    "dataVintage": {
                      "type": "array",
                      "description": "The dated editions behind this answer — figures a publisher set or measured for a named year. Present only where the answer depends on one; arithmetic endpoints omit it.",
                      "items": {
                        "type": "object",
                        "required": [
                          "id",
                          "publisher",
                          "edition",
                          "dataYear",
                          "cycleYears",
                          "supersededFrom",
                          "source"
                        ],
                        "properties": {
                          "id": {
                            "type": "string",
                            "description": "Stable identifier for the edition, matching DATA_VINTAGES in @calcwise/finance."
                          },
                          "publisher": {
                            "type": "string",
                            "description": "The body that measured or set these figures."
                          },
                          "edition": {
                            "type": "string",
                            "description": "The specific publication the figures were transcribed from."
                          },
                          "dataYear": {
                            "type": "integer",
                            "description": "The year the figures describe, or are in force for."
                          },
                          "cycleYears": {
                            "type": "integer",
                            "description": "Years between editions: 1 for an annual series, 3 for the triennial SCF."
                          },
                          "supersededFrom": {
                            "type": "string",
                            "format": "date",
                            "description": "The ISO date (UTC) from which a newer edition is reliably published and this one is a year behind. Compare it against your own clock — this response is cacheable, so it carries no live verdict about its own freshness."
                          },
                          "source": {
                            "type": "string",
                            "description": "The page or notice the current figures were read from."
                          }
                        }
                      }
                    }
                  }
                }
              },
              "required": [
                "endpoint",
                "inputs",
                "result",
                "meta"
              ]
            }
          }
        }
      },
      "budget-result": {
        "description": "The 50/30/20 budget answer.",
        "headers": {
          "Cache-Control": {
            "description": "Long-lived: the same URL is the same answer forever.",
            "schema": {
              "type": "string"
            }
          },
          "X-RateLimit-Remaining": {
            "description": "Requests left in the current window.",
            "schema": {
              "type": "integer"
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "properties": {
                "endpoint": {
                  "type": "string",
                  "const": "budget"
                },
                "inputs": {
                  "type": "object",
                  "description": "The parsed inputs the answer was computed from: your request with defaults filled in, money parsed out of strings like \"$400,000\", and enum spellings canonicalised.",
                  "properties": {
                    "income": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 10000000,
                      "x-unit": "usd",
                      "description": "Monthly **take-home** pay — after taxes and payroll deductions."
                    },
                    "housing": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 10000000,
                      "x-unit": "usd",
                      "description": "Monthly housing cost **including utilities**, HUD's basis. Drives the whole `housing` object; omit it and that comes back null. Echoed only when sent."
                    },
                    "grossMonthlyIncome": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 10000000,
                      "x-unit": "usd",
                      "description": "Monthly income **before** taxes. Used for nothing but HUD's cost-burden test, which is defined on gross income. Omit it and `housing.hud` comes back null rather than being estimated from take-home. Echoed only when sent."
                    },
                    "annualIncome": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 1000000000,
                      "x-unit": "usd",
                      "description": "Annual **pre-tax family** income. Selects `margin.band` only — it changes no arithmetic. Omit it and the band comes back null. Echoed only when sent."
                    }
                  },
                  "required": [
                    "income"
                  ]
                },
                "result": {
                  "type": "object",
                  "description": "The answer.",
                  "properties": {
                    "income": {
                      "description": "Monthly take-home pay, echoed."
                    },
                    "plan": {
                      "description": "The rule's three slices in dollars a month: `needs` (50%), `wants` (30%) and `savings` (20%)."
                    },
                    "savingsPerYear": {
                      "description": "The savings slice over twelve months, at this pace and ignoring investment returns."
                    },
                    "ruleShares": {
                      "description": "The three percentages, so a caller never hardcodes 50/30/20 itself."
                    },
                    "ruleOrigin": {
                      "description": "Where the rule comes from — a 2005 book, named. See the notes."
                    },
                    "housing": {
                      "description": "Housing against the budget: `sharePct` of take-home, **shareOfNeedsBudgetPct** of the rule's own needs slice, `leavesForOtherNeeds` in dollars, and `fitsNeedsBudget`. Null when no `housing` was sent. Its nested `hud` carries HUD's cost-burden test on HUD's own basis — `grossSharePct`, `costBurdened` (above 30% of gross) and `severelyCostBurdened` (above 50%) — and is itself null unless `grossMonthlyIncome` was supplied, per the notes."
                    },
                    "margin": {
                      "description": "What the Federal Reserve measures about having anything left over at month end: `alwaysOrOftenPct` across all US adults, the `band` matching `annualIncome`, and every `bands` entry with its full five-point distribution."
                    }
                  }
                },
                "notes": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                },
                "meta": {
                  "type": "object",
                  "description": "Provenance: the source site, the matching calculator page, the docs, the disclaimer, and dataVintage — the dated editions this answer depends on.",
                  "properties": {
                    "source": {
                      "type": "string"
                    },
                    "calculator": {
                      "type": "string"
                    },
                    "docs": {
                      "type": "string"
                    },
                    "disclaimer": {
                      "type": "string"
                    },
                    "dataVintage": {
                      "type": "array",
                      "description": "The dated editions behind this answer — figures a publisher set or measured for a named year. Present only where the answer depends on one; arithmetic endpoints omit it.",
                      "items": {
                        "type": "object",
                        "required": [
                          "id",
                          "publisher",
                          "edition",
                          "dataYear",
                          "cycleYears",
                          "supersededFrom",
                          "source"
                        ],
                        "properties": {
                          "id": {
                            "type": "string",
                            "description": "Stable identifier for the edition, matching DATA_VINTAGES in @calcwise/finance."
                          },
                          "publisher": {
                            "type": "string",
                            "description": "The body that measured or set these figures."
                          },
                          "edition": {
                            "type": "string",
                            "description": "The specific publication the figures were transcribed from."
                          },
                          "dataYear": {
                            "type": "integer",
                            "description": "The year the figures describe, or are in force for."
                          },
                          "cycleYears": {
                            "type": "integer",
                            "description": "Years between editions: 1 for an annual series, 3 for the triennial SCF."
                          },
                          "supersededFrom": {
                            "type": "string",
                            "format": "date",
                            "description": "The ISO date (UTC) from which a newer edition is reliably published and this one is a year behind. Compare it against your own clock — this response is cacheable, so it carries no live verdict about its own freshness."
                          },
                          "source": {
                            "type": "string",
                            "description": "The page or notice the current figures were read from."
                          }
                        }
                      }
                    }
                  }
                }
              },
              "required": [
                "endpoint",
                "inputs",
                "result",
                "meta"
              ]
            }
          }
        }
      },
      "cd-result": {
        "description": "The certificate of deposit answer.",
        "headers": {
          "Cache-Control": {
            "description": "Long-lived: the same URL is the same answer forever.",
            "schema": {
              "type": "string"
            }
          },
          "X-RateLimit-Remaining": {
            "description": "Requests left in the current window.",
            "schema": {
              "type": "integer"
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "properties": {
                "endpoint": {
                  "type": "string",
                  "const": "cd"
                },
                "inputs": {
                  "type": "object",
                  "description": "The parsed inputs the answer was computed from: your request with defaults filled in, money parsed out of strings like \"$400,000\", and enum spellings canonicalised.",
                  "properties": {
                    "deposit": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 10000000000,
                      "x-unit": "usd",
                      "description": "Amount deposited."
                    },
                    "apy": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 50,
                      "x-unit": "percent",
                      "description": "Annual percentage yield, as quoted."
                    },
                    "termMonths": {
                      "type": "integer",
                      "minimum": 1,
                      "maximum": 600,
                      "x-unit": "months",
                      "description": "Term of the CD in months."
                    }
                  },
                  "required": [
                    "deposit",
                    "apy",
                    "termMonths"
                  ]
                },
                "result": {
                  "type": "object",
                  "description": "The answer.",
                  "properties": {
                    "maturity": {
                      "description": "Value when the term ends."
                    },
                    "interest": {
                      "description": "Maturity minus the deposit."
                    }
                  }
                },
                "notes": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                },
                "warnings": {
                  "type": "array",
                  "description": "Values that were accepted but may not say what you meant. Percentages here are whole numbers, so 6.5 means 6.5%; a percent parameter sent 0.065 is read as 0.065% and warned about rather than silently rescaled. Absent when there is nothing to say.",
                  "items": {
                    "type": "object",
                    "required": [
                      "param",
                      "code",
                      "message",
                      "sent",
                      "didYouMean"
                    ],
                    "properties": {
                      "param": {
                        "type": "string",
                        "description": "The parameter the warning is about."
                      },
                      "code": {
                        "type": "string",
                        "enum": [
                          "percent_looks_like_fraction"
                        ],
                        "description": "The machine-readable kind. Switch on this rather than on the message."
                      },
                      "message": {
                        "type": "string",
                        "description": "One sentence: what was read, and what to send instead."
                      },
                      "sent": {
                        "type": "number",
                        "description": "The value you sent, as it was parsed."
                      },
                      "didYouMean": {
                        "type": "number",
                        "description": "The value you probably meant — retry with this."
                      }
                    }
                  }
                },
                "meta": {
                  "type": "object",
                  "description": "Provenance: the source site, the matching calculator page, the docs and the disclaimer.",
                  "properties": {
                    "source": {
                      "type": "string"
                    },
                    "calculator": {
                      "type": "string"
                    },
                    "docs": {
                      "type": "string"
                    },
                    "disclaimer": {
                      "type": "string"
                    }
                  }
                }
              },
              "required": [
                "endpoint",
                "inputs",
                "result",
                "meta"
              ]
            }
          }
        }
      },
      "inflation-result": {
        "description": "The inflation impact answer.",
        "headers": {
          "Cache-Control": {
            "description": "Long-lived: the same URL is the same answer forever.",
            "schema": {
              "type": "string"
            }
          },
          "X-RateLimit-Remaining": {
            "description": "Requests left in the current window.",
            "schema": {
              "type": "integer"
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "properties": {
                "endpoint": {
                  "type": "string",
                  "const": "inflation"
                },
                "inputs": {
                  "type": "object",
                  "description": "The parsed inputs the answer was computed from: your request with defaults filled in, money parsed out of strings like \"$400,000\", and enum spellings canonicalised.",
                  "properties": {
                    "amount": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 1000000000000,
                      "x-unit": "usd",
                      "description": "Today's amount."
                    },
                    "years": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 200,
                      "x-unit": "years",
                      "description": "How many years forward."
                    },
                    "rate": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 100,
                      "x-unit": "percent",
                      "description": "Average annual inflation rate. Always echoed; 3 when not sent."
                    }
                  },
                  "required": [
                    "amount",
                    "years",
                    "rate"
                  ]
                },
                "result": {
                  "type": "object",
                  "description": "The answer.",
                  "properties": {
                    "factor": {
                      "description": "Cumulative price multiplier over the period."
                    },
                    "futureCost": {
                      "description": "What today's basket costs then."
                    },
                    "buyingPower": {
                      "description": "What today's money is worth then, in today's dollars."
                    },
                    "lostPercent": {
                      "description": "Share of buying power lost, as a percentage."
                    }
                  }
                },
                "warnings": {
                  "type": "array",
                  "description": "Values that were accepted but may not say what you meant. Percentages here are whole numbers, so 6.5 means 6.5%; a percent parameter sent 0.065 is read as 0.065% and warned about rather than silently rescaled. Absent when there is nothing to say.",
                  "items": {
                    "type": "object",
                    "required": [
                      "param",
                      "code",
                      "message",
                      "sent",
                      "didYouMean"
                    ],
                    "properties": {
                      "param": {
                        "type": "string",
                        "description": "The parameter the warning is about."
                      },
                      "code": {
                        "type": "string",
                        "enum": [
                          "percent_looks_like_fraction"
                        ],
                        "description": "The machine-readable kind. Switch on this rather than on the message."
                      },
                      "message": {
                        "type": "string",
                        "description": "One sentence: what was read, and what to send instead."
                      },
                      "sent": {
                        "type": "number",
                        "description": "The value you sent, as it was parsed."
                      },
                      "didYouMean": {
                        "type": "number",
                        "description": "The value you probably meant — retry with this."
                      }
                    }
                  }
                },
                "meta": {
                  "type": "object",
                  "description": "Provenance: the source site, the matching calculator page, the docs and the disclaimer.",
                  "properties": {
                    "source": {
                      "type": "string"
                    },
                    "calculator": {
                      "type": "string"
                    },
                    "docs": {
                      "type": "string"
                    },
                    "disclaimer": {
                      "type": "string"
                    }
                  }
                }
              },
              "required": [
                "endpoint",
                "inputs",
                "result",
                "meta"
              ]
            }
          }
        }
      },
      "401k-result": {
        "description": "The 401(k) projection answer.",
        "headers": {
          "Cache-Control": {
            "description": "Long-lived: the same URL is the same answer forever.",
            "schema": {
              "type": "string"
            }
          },
          "X-RateLimit-Remaining": {
            "description": "Requests left in the current window.",
            "schema": {
              "type": "integer"
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "properties": {
                "endpoint": {
                  "type": "string",
                  "const": "401k"
                },
                "inputs": {
                  "type": "object",
                  "description": "The parsed inputs the answer was computed from: your request with defaults filled in, money parsed out of strings like \"$400,000\", and enum spellings canonicalised.",
                  "properties": {
                    "currentBalance": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 10000000000,
                      "x-unit": "usd",
                      "description": "What is in the account today. Always echoed; 0 when not sent."
                    },
                    "salary": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 1000000000,
                      "x-unit": "usd",
                      "description": "Current annual salary."
                    },
                    "contribPercent": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 100,
                      "x-unit": "percent",
                      "description": "Share of salary you defer each year."
                    },
                    "matchRatePercent": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 200,
                      "x-unit": "percent",
                      "description": "Cents on the dollar the employer matches — 50 means 50%. Always echoed; 50 when not sent."
                    },
                    "matchLimitPercent": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 100,
                      "x-unit": "percent",
                      "description": "Share of salary the match applies up to. Always echoed; 6 when not sent."
                    },
                    "rate": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 100,
                      "x-unit": "percent",
                      "description": "Average annual return. Always echoed; 7 when not sent."
                    },
                    "annualRaisePercent": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 50,
                      "x-unit": "percent",
                      "description": "Average yearly raise. Always echoed; 2 when not sent."
                    },
                    "years": {
                      "type": "integer",
                      "minimum": 0,
                      "maximum": 70,
                      "x-unit": "years",
                      "description": "Years until you stop contributing."
                    },
                    "currentAge": {
                      "type": "integer",
                      "minimum": 0,
                      "maximum": 100,
                      "x-unit": "years",
                      "description": "The age you reach this year. Given it, each year of the projection uses that year's own § 402(g) limit, including the age-50 and age-60-to-63 catch-ups. Ignored when deferralLimit is sent. Echoed only when sent."
                    },
                    "deferralLimit": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 1000000,
                      "x-unit": "usd",
                      "description": "Employee deferral cap for every year, overriding currentAge. Omit both for the flat 2026 limit, $24,500. Echoed only when sent."
                    },
                    "series": {
                      "type": "boolean",
                      "description": "Include the year-by-year balance series. Always echoed; false when not sent."
                    }
                  },
                  "required": [
                    "currentBalance",
                    "salary",
                    "contribPercent",
                    "matchRatePercent",
                    "matchLimitPercent",
                    "rate",
                    "annualRaisePercent",
                    "years",
                    "series"
                  ]
                },
                "result": {
                  "type": "object",
                  "description": "The answer.",
                  "properties": {
                    "balance": {
                      "description": "Projected balance at the end."
                    },
                    "yourContributions": {
                      "description": "Everything you put in, excluding the starting balance."
                    },
                    "employerContributions": {
                      "description": "Everything the employer matched in."
                    },
                    "growth": {
                      "description": "Investment growth on top."
                    },
                    "cappedByDeferralLimit": {
                      "description": "True if your contribution percentage would exceed the annual cap."
                    },
                    "catchUpContributions": {
                      "description": "Of yourContributions, the dollars only a catch-up allowed. 0 unless currentAge was sent and the plain limit would have bitten."
                    },
                    "series": {
                      "description": "Present only when series=true."
                    }
                  }
                },
                "notes": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                },
                "warnings": {
                  "type": "array",
                  "description": "Values that were accepted but may not say what you meant. Percentages here are whole numbers, so 6.5 means 6.5%; a percent parameter sent 0.065 is read as 0.065% and warned about rather than silently rescaled. Absent when there is nothing to say.",
                  "items": {
                    "type": "object",
                    "required": [
                      "param",
                      "code",
                      "message",
                      "sent",
                      "didYouMean"
                    ],
                    "properties": {
                      "param": {
                        "type": "string",
                        "description": "The parameter the warning is about."
                      },
                      "code": {
                        "type": "string",
                        "enum": [
                          "percent_looks_like_fraction"
                        ],
                        "description": "The machine-readable kind. Switch on this rather than on the message."
                      },
                      "message": {
                        "type": "string",
                        "description": "One sentence: what was read, and what to send instead."
                      },
                      "sent": {
                        "type": "number",
                        "description": "The value you sent, as it was parsed."
                      },
                      "didYouMean": {
                        "type": "number",
                        "description": "The value you probably meant — retry with this."
                      }
                    }
                  }
                },
                "meta": {
                  "type": "object",
                  "description": "Provenance: the source site, the matching calculator page, the docs, the disclaimer, and dataVintage — the dated editions this answer depends on.",
                  "properties": {
                    "source": {
                      "type": "string"
                    },
                    "calculator": {
                      "type": "string"
                    },
                    "docs": {
                      "type": "string"
                    },
                    "disclaimer": {
                      "type": "string"
                    },
                    "dataVintage": {
                      "type": "array",
                      "description": "The dated editions behind this answer — figures a publisher set or measured for a named year. Present only where the answer depends on one; arithmetic endpoints omit it.",
                      "items": {
                        "type": "object",
                        "required": [
                          "id",
                          "publisher",
                          "edition",
                          "dataYear",
                          "cycleYears",
                          "supersededFrom",
                          "source"
                        ],
                        "properties": {
                          "id": {
                            "type": "string",
                            "description": "Stable identifier for the edition, matching DATA_VINTAGES in @calcwise/finance."
                          },
                          "publisher": {
                            "type": "string",
                            "description": "The body that measured or set these figures."
                          },
                          "edition": {
                            "type": "string",
                            "description": "The specific publication the figures were transcribed from."
                          },
                          "dataYear": {
                            "type": "integer",
                            "description": "The year the figures describe, or are in force for."
                          },
                          "cycleYears": {
                            "type": "integer",
                            "description": "Years between editions: 1 for an annual series, 3 for the triennial SCF."
                          },
                          "supersededFrom": {
                            "type": "string",
                            "format": "date",
                            "description": "The ISO date (UTC) from which a newer edition is reliably published and this one is a year behind. Compare it against your own clock — this response is cacheable, so it carries no live verdict about its own freshness."
                          },
                          "source": {
                            "type": "string",
                            "description": "The page or notice the current figures were read from."
                          }
                        }
                      }
                    }
                  }
                }
              },
              "required": [
                "endpoint",
                "inputs",
                "result",
                "meta"
              ]
            }
          }
        }
      },
      "401k-match-result": {
        "description": "The employer match answer.",
        "headers": {
          "Cache-Control": {
            "description": "Long-lived: the same URL is the same answer forever.",
            "schema": {
              "type": "string"
            }
          },
          "X-RateLimit-Remaining": {
            "description": "Requests left in the current window.",
            "schema": {
              "type": "integer"
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "properties": {
                "endpoint": {
                  "type": "string",
                  "const": "401k-match"
                },
                "inputs": {
                  "type": "object",
                  "description": "The parsed inputs the answer was computed from: your request with defaults filled in, money parsed out of strings like \"$400,000\", and enum spellings canonicalised.",
                  "properties": {
                    "salary": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 1000000000,
                      "x-unit": "usd",
                      "description": "Annual salary."
                    },
                    "contribPercent": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 100,
                      "x-unit": "percent",
                      "description": "Share of salary you contribute."
                    },
                    "matchRatePercent": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 200,
                      "x-unit": "percent",
                      "description": "Cents on the dollar matched — 50 means 50%, 100 means dollar for dollar. Always echoed; 50 when not sent."
                    },
                    "matchLimitPercent": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 100,
                      "x-unit": "percent",
                      "description": "Share of salary the match applies up to. Always echoed; 6 when not sent."
                    }
                  },
                  "required": [
                    "salary",
                    "contribPercent",
                    "matchRatePercent",
                    "matchLimitPercent"
                  ]
                },
                "result": {
                  "type": "object",
                  "description": "The answer.",
                  "properties": {
                    "yourContribution": {
                      "description": "Your dollars in this year."
                    },
                    "match": {
                      "description": "Employer dollars you earn."
                    },
                    "maxMatch": {
                      "description": "The most the employer would put in at this formula."
                    },
                    "missed": {
                      "description": "Match you are giving up by contributing less than the limit."
                    },
                    "total": {
                      "description": "Your contribution plus the match."
                    }
                  }
                },
                "warnings": {
                  "type": "array",
                  "description": "Values that were accepted but may not say what you meant. Percentages here are whole numbers, so 6.5 means 6.5%; a percent parameter sent 0.065 is read as 0.065% and warned about rather than silently rescaled. Absent when there is nothing to say.",
                  "items": {
                    "type": "object",
                    "required": [
                      "param",
                      "code",
                      "message",
                      "sent",
                      "didYouMean"
                    ],
                    "properties": {
                      "param": {
                        "type": "string",
                        "description": "The parameter the warning is about."
                      },
                      "code": {
                        "type": "string",
                        "enum": [
                          "percent_looks_like_fraction"
                        ],
                        "description": "The machine-readable kind. Switch on this rather than on the message."
                      },
                      "message": {
                        "type": "string",
                        "description": "One sentence: what was read, and what to send instead."
                      },
                      "sent": {
                        "type": "number",
                        "description": "The value you sent, as it was parsed."
                      },
                      "didYouMean": {
                        "type": "number",
                        "description": "The value you probably meant — retry with this."
                      }
                    }
                  }
                },
                "meta": {
                  "type": "object",
                  "description": "Provenance: the source site, the matching calculator page, the docs and the disclaimer.",
                  "properties": {
                    "source": {
                      "type": "string"
                    },
                    "calculator": {
                      "type": "string"
                    },
                    "docs": {
                      "type": "string"
                    },
                    "disclaimer": {
                      "type": "string"
                    }
                  }
                }
              },
              "required": [
                "endpoint",
                "inputs",
                "result",
                "meta"
              ]
            }
          }
        }
      },
      "safe-withdrawal-result": {
        "description": "The safe withdrawal rate answer.",
        "headers": {
          "Cache-Control": {
            "description": "Long-lived: the same URL is the same answer forever.",
            "schema": {
              "type": "string"
            }
          },
          "X-RateLimit-Remaining": {
            "description": "Requests left in the current window.",
            "schema": {
              "type": "integer"
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "properties": {
                "endpoint": {
                  "type": "string",
                  "const": "safe-withdrawal"
                },
                "inputs": {
                  "type": "object",
                  "description": "The parsed inputs the answer was computed from: your request with defaults filled in, money parsed out of strings like \"$400,000\", and enum spellings canonicalised.",
                  "properties": {
                    "nestEgg": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 1000000000000,
                      "x-unit": "usd",
                      "description": "Portfolio value at retirement."
                    },
                    "withdrawalRate": {
                      "type": "number",
                      "minimum": 0.1,
                      "maximum": 20,
                      "x-unit": "percent",
                      "description": "First-year withdrawal rate. Always echoed; 4 when not sent."
                    }
                  },
                  "required": [
                    "nestEgg",
                    "withdrawalRate"
                  ]
                },
                "result": {
                  "type": "object",
                  "description": "The answer.",
                  "properties": {
                    "annual": {
                      "description": "First-year withdrawal in dollars."
                    },
                    "monthly": {
                      "description": "That divided by 12."
                    },
                    "multiple": {
                      "description": "Years of spending the portfolio represents — 25 at 4%."
                    }
                  }
                },
                "notes": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                },
                "warnings": {
                  "type": "array",
                  "description": "Values that were accepted but may not say what you meant. Percentages here are whole numbers, so 6.5 means 6.5%; a percent parameter sent 0.065 is read as 0.065% and warned about rather than silently rescaled. Absent when there is nothing to say.",
                  "items": {
                    "type": "object",
                    "required": [
                      "param",
                      "code",
                      "message",
                      "sent",
                      "didYouMean"
                    ],
                    "properties": {
                      "param": {
                        "type": "string",
                        "description": "The parameter the warning is about."
                      },
                      "code": {
                        "type": "string",
                        "enum": [
                          "percent_looks_like_fraction"
                        ],
                        "description": "The machine-readable kind. Switch on this rather than on the message."
                      },
                      "message": {
                        "type": "string",
                        "description": "One sentence: what was read, and what to send instead."
                      },
                      "sent": {
                        "type": "number",
                        "description": "The value you sent, as it was parsed."
                      },
                      "didYouMean": {
                        "type": "number",
                        "description": "The value you probably meant — retry with this."
                      }
                    }
                  }
                },
                "meta": {
                  "type": "object",
                  "description": "Provenance: the source site, the matching calculator page, the docs and the disclaimer.",
                  "properties": {
                    "source": {
                      "type": "string"
                    },
                    "calculator": {
                      "type": "string"
                    },
                    "docs": {
                      "type": "string"
                    },
                    "disclaimer": {
                      "type": "string"
                    }
                  }
                }
              },
              "required": [
                "endpoint",
                "inputs",
                "result",
                "meta"
              ]
            }
          }
        }
      },
      "fire-result": {
        "description": "The fire number and date answer.",
        "headers": {
          "Cache-Control": {
            "description": "Long-lived: the same URL is the same answer forever.",
            "schema": {
              "type": "string"
            }
          },
          "X-RateLimit-Remaining": {
            "description": "Requests left in the current window.",
            "schema": {
              "type": "integer"
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "properties": {
                "endpoint": {
                  "type": "string",
                  "const": "fire"
                },
                "inputs": {
                  "type": "object",
                  "description": "The parsed inputs the answer was computed from: your request with defaults filled in, money parsed out of strings like \"$400,000\", and enum spellings canonicalised.",
                  "properties": {
                    "annualSpending": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 1000000000,
                      "x-unit": "usd",
                      "description": "What you expect to spend per year in retirement."
                    },
                    "current": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 1000000000000,
                      "x-unit": "usd",
                      "description": "Invested savings today. Always echoed; 0 when not sent."
                    },
                    "annualSavings": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 1000000000,
                      "x-unit": "usd",
                      "description": "What you invest per year. Always echoed; 0 when not sent."
                    },
                    "rate": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 100,
                      "x-unit": "percent",
                      "description": "Real (after-inflation) annual return, so the answer is already in today's dollars. Always echoed; 7 when not sent."
                    },
                    "withdrawalRate": {
                      "type": "number",
                      "minimum": 0.1,
                      "maximum": 20,
                      "x-unit": "percent",
                      "description": "Withdrawal rate used to set the target. Always echoed; 4 when not sent."
                    }
                  },
                  "required": [
                    "annualSpending",
                    "current",
                    "annualSavings",
                    "rate",
                    "withdrawalRate"
                  ]
                },
                "result": {
                  "type": "object",
                  "description": "The answer.",
                  "properties": {
                    "fireNumber": {
                      "description": "Annual spending divided by the withdrawal rate."
                    },
                    "reached": {
                      "description": "Whether saving gets there inside 70 years."
                    },
                    "years": {
                      "description": "Years until the target is hit. 0 if you are already there."
                    },
                    "balances": {
                      "description": "Balance at the end of each year, index 0 being today."
                    }
                  }
                },
                "notes": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                },
                "warnings": {
                  "type": "array",
                  "description": "Values that were accepted but may not say what you meant. Percentages here are whole numbers, so 6.5 means 6.5%; a percent parameter sent 0.065 is read as 0.065% and warned about rather than silently rescaled. Absent when there is nothing to say.",
                  "items": {
                    "type": "object",
                    "required": [
                      "param",
                      "code",
                      "message",
                      "sent",
                      "didYouMean"
                    ],
                    "properties": {
                      "param": {
                        "type": "string",
                        "description": "The parameter the warning is about."
                      },
                      "code": {
                        "type": "string",
                        "enum": [
                          "percent_looks_like_fraction"
                        ],
                        "description": "The machine-readable kind. Switch on this rather than on the message."
                      },
                      "message": {
                        "type": "string",
                        "description": "One sentence: what was read, and what to send instead."
                      },
                      "sent": {
                        "type": "number",
                        "description": "The value you sent, as it was parsed."
                      },
                      "didYouMean": {
                        "type": "number",
                        "description": "The value you probably meant — retry with this."
                      }
                    }
                  }
                },
                "meta": {
                  "type": "object",
                  "description": "Provenance: the source site, the matching calculator page, the docs and the disclaimer.",
                  "properties": {
                    "source": {
                      "type": "string"
                    },
                    "calculator": {
                      "type": "string"
                    },
                    "docs": {
                      "type": "string"
                    },
                    "disclaimer": {
                      "type": "string"
                    }
                  }
                }
              },
              "required": [
                "endpoint",
                "inputs",
                "result",
                "meta"
              ]
            }
          }
        }
      },
      "coast-fire-result": {
        "description": "The coast fire answer.",
        "headers": {
          "Cache-Control": {
            "description": "Long-lived: the same URL is the same answer forever.",
            "schema": {
              "type": "string"
            }
          },
          "X-RateLimit-Remaining": {
            "description": "Requests left in the current window.",
            "schema": {
              "type": "integer"
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "properties": {
                "endpoint": {
                  "type": "string",
                  "const": "coast-fire"
                },
                "inputs": {
                  "type": "object",
                  "description": "The parsed inputs the answer was computed from: your request with defaults filled in, money parsed out of strings like \"$400,000\", and enum spellings canonicalised.",
                  "properties": {
                    "annualSpending": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 1000000000,
                      "x-unit": "usd",
                      "description": "Expected annual spending in retirement."
                    },
                    "current": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 1000000000000,
                      "x-unit": "usd",
                      "description": "Invested savings today. Always echoed; 0 when not sent."
                    },
                    "annualSavings": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 1000000000,
                      "x-unit": "usd",
                      "description": "What you invest per year until you coast. Always echoed; 0 when not sent."
                    },
                    "yearsToRetirement": {
                      "type": "integer",
                      "minimum": 0,
                      "maximum": 70,
                      "x-unit": "years",
                      "description": "Years until you plan to retire."
                    },
                    "rate": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 100,
                      "x-unit": "percent",
                      "description": "Real (after-inflation) annual return. Always echoed; 7 when not sent."
                    },
                    "withdrawalRate": {
                      "type": "number",
                      "minimum": 0.1,
                      "maximum": 20,
                      "x-unit": "percent",
                      "description": "Withdrawal rate used to set the target. Always echoed; 4 when not sent."
                    }
                  },
                  "required": [
                    "annualSpending",
                    "current",
                    "annualSavings",
                    "yearsToRetirement",
                    "rate",
                    "withdrawalRate"
                  ]
                },
                "result": {
                  "type": "object",
                  "description": "The answer.",
                  "properties": {
                    "fireNumber": {
                      "description": "The full retirement target."
                    },
                    "coastNumberToday": {
                      "description": "Invest this much today and you can stop contributing."
                    },
                    "reached": {
                      "description": "Whether you reach the coast point before retirement."
                    },
                    "yearsToCoast": {
                      "description": "Years until you can stop contributing. 0 if you already can."
                    }
                  }
                },
                "warnings": {
                  "type": "array",
                  "description": "Values that were accepted but may not say what you meant. Percentages here are whole numbers, so 6.5 means 6.5%; a percent parameter sent 0.065 is read as 0.065% and warned about rather than silently rescaled. Absent when there is nothing to say.",
                  "items": {
                    "type": "object",
                    "required": [
                      "param",
                      "code",
                      "message",
                      "sent",
                      "didYouMean"
                    ],
                    "properties": {
                      "param": {
                        "type": "string",
                        "description": "The parameter the warning is about."
                      },
                      "code": {
                        "type": "string",
                        "enum": [
                          "percent_looks_like_fraction"
                        ],
                        "description": "The machine-readable kind. Switch on this rather than on the message."
                      },
                      "message": {
                        "type": "string",
                        "description": "One sentence: what was read, and what to send instead."
                      },
                      "sent": {
                        "type": "number",
                        "description": "The value you sent, as it was parsed."
                      },
                      "didYouMean": {
                        "type": "number",
                        "description": "The value you probably meant — retry with this."
                      }
                    }
                  }
                },
                "meta": {
                  "type": "object",
                  "description": "Provenance: the source site, the matching calculator page, the docs and the disclaimer.",
                  "properties": {
                    "source": {
                      "type": "string"
                    },
                    "calculator": {
                      "type": "string"
                    },
                    "docs": {
                      "type": "string"
                    },
                    "disclaimer": {
                      "type": "string"
                    }
                  }
                }
              },
              "required": [
                "endpoint",
                "inputs",
                "result",
                "meta"
              ]
            }
          }
        }
      },
      "roth-vs-traditional-result": {
        "description": "The roth vs traditional answer.",
        "headers": {
          "Cache-Control": {
            "description": "Long-lived: the same URL is the same answer forever.",
            "schema": {
              "type": "string"
            }
          },
          "X-RateLimit-Remaining": {
            "description": "Requests left in the current window.",
            "schema": {
              "type": "integer"
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "properties": {
                "endpoint": {
                  "type": "string",
                  "const": "roth-vs-traditional"
                },
                "inputs": {
                  "type": "object",
                  "description": "The parsed inputs the answer was computed from: your request with defaults filled in, money parsed out of strings like \"$400,000\", and enum spellings canonicalised.",
                  "properties": {
                    "contribution": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 100000,
                      "x-unit": "usd",
                      "description": "Yearly contribution, measured as **what leaves your pocket after tax**. Defaults to the 2026 IRA limit. See the notes — this is not the same as the figure you type into a brokerage form. Always echoed; 7500 when not sent."
                    },
                    "years": {
                      "type": "integer",
                      "minimum": 1,
                      "maximum": 60,
                      "x-unit": "years",
                      "description": "Years of contributions and growth before you start withdrawing. Always echoed; 30 when not sent."
                    },
                    "rate": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 20,
                      "x-unit": "percent",
                      "description": "Annual return, the same for both accounts. Always echoed; 7 when not sent."
                    },
                    "taxNow": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 100,
                      "x-unit": "percent",
                      "description": "Your marginal tax rate today — the bracket the contribution would otherwise be taxed in."
                    },
                    "taxRetire": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 100,
                      "x-unit": "percent",
                      "description": "The rate you expect the Traditional withdrawal to pay. **Send the effective rate, not a bracket** — `withdrawal` below computes it for you."
                    },
                    "withdrawal": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 10000000,
                      "x-unit": "usd",
                      "description": "A year's planned withdrawal from the pre-tax account. Drives the whole `retirementRate` object, which is the point of this endpoint; omit it and that comes back null. Echoed only when sent."
                    },
                    "otherTaxableIncome": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 10000000,
                      "x-unit": "usd",
                      "description": "Other ordinary taxable income in the same retirement year — a pension, a spouse's wages, another account's required distribution. The withdrawal stacks on top of it, so this raises the rate. Social Security is not modelled; see the notes. Always echoed; 0 when not sent."
                    },
                    "withheldPct": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 100,
                      "x-unit": "percent",
                      "description": "What the payer actually withheld, if you have the statement in front of you. Omit it and `withholding` uses the default for `account` — 20% or 10% — and flags it as assumed. Withholding is a deposit, not a rate; it changes no arithmetic above. Echoed only when sent."
                    },
                    "account": {
                      "type": "string",
                      "enum": [
                        "plan",
                        "ira"
                      ],
                      "description": "Where the withdrawal is paid from, which sets the default withholding when `withheldPct` is omitted. A workplace plan withholds 20% and you cannot decline it; an IRA withholds 10% and you can elect out. Selects a default only — it changes no arithmetic. Always echoed; \"plan\" when not sent."
                    },
                    "age": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 120,
                      "x-unit": "years",
                      "description": "Your age **when the withdrawal is taken**, which is a different age from `age50Plus` below — that one is about this year's contribution. Halves count, because the threshold is one: 59.4 is early and 59.5 is not. Selects `ageRules`, which also needs `withdrawal`; omit it and that comes back null. Echoed only when sent."
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "single",
                        "married",
                        "hoh",
                        "mfs"
                      ],
                      "description": "Filing status in retirement, which sets the standard deduction and the brackets the withdrawal fills. Always echoed; \"single\" when not sent."
                    },
                    "magi": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 100000000,
                      "x-unit": "usd",
                      "description": "Today's modified adjusted gross income. Selects `eligibility` only — it changes no arithmetic above. Omit it and that comes back null. Echoed only when sent."
                    },
                    "age50Plus": {
                      "type": "boolean",
                      "description": "Whether the catch-up limit applies, which raises the cap from $7,500 to $8,600. This is your age **today**, in the contribution year — `age` above is your age at the withdrawal, decades later. Affects `limitEdge` and `eligibility`. Always echoed; false when not sent."
                    }
                  },
                  "required": [
                    "contribution",
                    "years",
                    "rate",
                    "taxNow",
                    "taxRetire",
                    "otherTaxableIncome",
                    "account",
                    "status",
                    "age50Plus"
                  ]
                },
                "result": {
                  "type": "object",
                  "description": "The answer.",
                  "properties": {
                    "winner": {
                      "description": "`\"roth\"`, `\"traditional\"` or `\"tie\"`.",
                      "type": "string",
                      "enum": [
                        "roth",
                        "traditional",
                        "tie"
                      ]
                    },
                    "roth": {
                      "description": "After-tax value of the Roth at the end. Withdrawals are tax-free, so this is the balance."
                    },
                    "traditional": {
                      "description": "After-tax value of the Traditional at the end, net of the exit tax at `taxRetire`."
                    },
                    "difference": {
                      "description": "`traditional − roth`. Positive means the Traditional came out ahead."
                    },
                    "edge": {
                      "description": "The absolute difference, which is what a headline wants."
                    },
                    "tie": {
                      "description": "True when the two land within half a percent — which is what equal tax rates produce."
                    },
                    "retirementRate": {
                      "description": "**The correction this endpoint exists for.** The federal tax a `withdrawal` actually causes: `effectiveRatePct` against the `marginalRatePct` everyone quotes, `overstatementPct` between them, the `untaxedAmount` the standard deduction shields, and the `bands` the withdrawal itself occupies. `assumesOnlyIncome` flags the best case. Null unless `withdrawal` was sent."
                    },
                    "withholding": {
                      "description": "**What the payer took, against what is actually owed.** `withheldPct` and the dollars `withheld`, the `tax` due on the same withdrawal, and the `gap` between them — `overwithheld` is true when that gap is positive, which means the excess comes back as a refund the following spring, having earned nothing in between. `breakEvenWithdrawal` is the withdrawal at which that withholding rate would finally be the right one; below it the payer always takes too much. `assumedPct` is true when the rate was the default for `account` rather than one you sent. Null unless `withdrawal` was sent."
                    },
                    "ageRules": {
                      "description": "**What the two ages cost, priced.** Before 59½ an additional tax applies, and it is charged on the distribution rather than on the tax — so `penalty` can exceed `incomeTax` outright, and `allInRatePct` is simply the effective rate plus `penaltyPct`. `early` says which side of the threshold you are on, `yearsUntilPenaltyFree` how far, and `assumesNoException` flags that none of the 72(t) exceptions are modelled. The other age is reported rather than computed: `rmdAgeReached` and `yearsUntilRmd` against `rmdAge`, the age at which the withdrawal stops being a choice. Null unless both `age` and `withdrawal` were sent."
                    },
                    "limitEdge": {
                      "description": "What the contribution cap does when you max out: the `traditionalAfterTaxCost` of the same nominal contribution, the `equivalentPreTaxContribution` that would match a full Roth, how far that `exceedsLimitBy`, and the resulting `rothAdvantagePct`. Nothing here depends on future tax rates."
                    },
                    "eligibility": {
                      "description": "Whether this `magi` may contribute to a Roth IRA at all: the `band`, the reduced `limit` after the IRS's rounding, and the `phaseOut` range. Null unless `magi` was sent."
                    },
                    "years": {
                      "description": "Whole years used, after rounding the `years` parameter."
                    },
                    "status": {
                      "description": "Filing status used for the deduction, the brackets and the phase-out range, echoed."
                    },
                    "taxYear": {
                      "description": "The tax year every bracket, deduction and limit above belongs to."
                    }
                  }
                },
                "notes": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                },
                "warnings": {
                  "type": "array",
                  "description": "Values that were accepted but may not say what you meant. Percentages here are whole numbers, so 6.5 means 6.5%; a percent parameter sent 0.065 is read as 0.065% and warned about rather than silently rescaled. Absent when there is nothing to say.",
                  "items": {
                    "type": "object",
                    "required": [
                      "param",
                      "code",
                      "message",
                      "sent",
                      "didYouMean"
                    ],
                    "properties": {
                      "param": {
                        "type": "string",
                        "description": "The parameter the warning is about."
                      },
                      "code": {
                        "type": "string",
                        "enum": [
                          "percent_looks_like_fraction"
                        ],
                        "description": "The machine-readable kind. Switch on this rather than on the message."
                      },
                      "message": {
                        "type": "string",
                        "description": "One sentence: what was read, and what to send instead."
                      },
                      "sent": {
                        "type": "number",
                        "description": "The value you sent, as it was parsed."
                      },
                      "didYouMean": {
                        "type": "number",
                        "description": "The value you probably meant — retry with this."
                      }
                    }
                  }
                },
                "meta": {
                  "type": "object",
                  "description": "Provenance: the source site, the matching calculator page, the docs, the disclaimer, and dataVintage — the dated editions this answer depends on.",
                  "properties": {
                    "source": {
                      "type": "string"
                    },
                    "calculator": {
                      "type": "string"
                    },
                    "docs": {
                      "type": "string"
                    },
                    "disclaimer": {
                      "type": "string"
                    },
                    "dataVintage": {
                      "type": "array",
                      "description": "The dated editions behind this answer — figures a publisher set or measured for a named year. Present only where the answer depends on one; arithmetic endpoints omit it.",
                      "items": {
                        "type": "object",
                        "required": [
                          "id",
                          "publisher",
                          "edition",
                          "dataYear",
                          "cycleYears",
                          "supersededFrom",
                          "source"
                        ],
                        "properties": {
                          "id": {
                            "type": "string",
                            "description": "Stable identifier for the edition, matching DATA_VINTAGES in @calcwise/finance."
                          },
                          "publisher": {
                            "type": "string",
                            "description": "The body that measured or set these figures."
                          },
                          "edition": {
                            "type": "string",
                            "description": "The specific publication the figures were transcribed from."
                          },
                          "dataYear": {
                            "type": "integer",
                            "description": "The year the figures describe, or are in force for."
                          },
                          "cycleYears": {
                            "type": "integer",
                            "description": "Years between editions: 1 for an annual series, 3 for the triennial SCF."
                          },
                          "supersededFrom": {
                            "type": "string",
                            "format": "date",
                            "description": "The ISO date (UTC) from which a newer edition is reliably published and this one is a year behind. Compare it against your own clock — this response is cacheable, so it carries no live verdict about its own freshness."
                          },
                          "source": {
                            "type": "string",
                            "description": "The page or notice the current figures were read from."
                          }
                        }
                      }
                    }
                  }
                }
              },
              "required": [
                "endpoint",
                "inputs",
                "result",
                "meta"
              ]
            }
          }
        }
      },
      "refinance-result": {
        "description": "The mortgage refinance break-even answer.",
        "headers": {
          "Cache-Control": {
            "description": "Long-lived: the same URL is the same answer forever.",
            "schema": {
              "type": "string"
            }
          },
          "X-RateLimit-Remaining": {
            "description": "Requests left in the current window.",
            "schema": {
              "type": "integer"
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "properties": {
                "endpoint": {
                  "type": "string",
                  "const": "refinance"
                },
                "inputs": {
                  "type": "object",
                  "description": "The parsed inputs the answer was computed from: your request with defaults filled in, money parsed out of strings like \"$400,000\", and enum spellings canonicalised.",
                  "properties": {
                    "balance": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 10000000000,
                      "x-unit": "usd",
                      "description": "What is still owed on the current mortgage."
                    },
                    "currentRate": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 100,
                      "x-unit": "percent",
                      "description": "The rate on the loan you have today."
                    },
                    "yearsLeft": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 50,
                      "x-unit": "years",
                      "description": "Years remaining on the current loan."
                    },
                    "newRate": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 100,
                      "x-unit": "percent",
                      "description": "The rate you are being offered."
                    },
                    "newTermYears": {
                      "type": "integer",
                      "minimum": 1,
                      "maximum": 50,
                      "x-unit": "years",
                      "description": "Term of the new loan, in years. Always echoed; 30 when not sent."
                    },
                    "closingCosts": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 100000000,
                      "x-unit": "usd",
                      "description": "Closing costs on the new loan, paid up front. Typically 2%–5% of the loan. Always echoed; 0 when not sent."
                    }
                  },
                  "required": [
                    "balance",
                    "currentRate",
                    "yearsLeft",
                    "newRate",
                    "newTermYears",
                    "closingCosts"
                  ]
                },
                "result": {
                  "type": "object",
                  "description": "The answer.",
                  "properties": {
                    "currentPayment": {
                      "description": "Principal and interest on the loan you have."
                    },
                    "newPayment": {
                      "description": "Principal and interest on the loan you would take."
                    },
                    "monthlySavings": {
                      "description": "Current payment minus new payment. Negative when refinancing costs more each month."
                    },
                    "breakEvenMonths": {
                      "description": "Months of saving needed to cover the closing costs. Null (as \"never\") when the payment goes up."
                    },
                    "breakEvenLabel": {
                      "description": "The same figure in words, e.g. \"1 yr 6 mo\"."
                    },
                    "currentInterest": {
                      "description": "Interest left on the current loan if you keep it to the end."
                    },
                    "newInterest": {
                      "description": "Interest on the new loan over its full term."
                    },
                    "lifetimeInterestDiff": {
                      "description": "Current interest minus new interest. Negative means the refinance costs more interest over its life, even at a lower rate."
                    },
                    "resetsTheTerm": {
                      "description": "True when the payment falls but the lifetime interest rises — the reset trap."
                    }
                  }
                },
                "notes": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                },
                "warnings": {
                  "type": "array",
                  "description": "Values that were accepted but may not say what you meant. Percentages here are whole numbers, so 6.5 means 6.5%; a percent parameter sent 0.065 is read as 0.065% and warned about rather than silently rescaled. Absent when there is nothing to say.",
                  "items": {
                    "type": "object",
                    "required": [
                      "param",
                      "code",
                      "message",
                      "sent",
                      "didYouMean"
                    ],
                    "properties": {
                      "param": {
                        "type": "string",
                        "description": "The parameter the warning is about."
                      },
                      "code": {
                        "type": "string",
                        "enum": [
                          "percent_looks_like_fraction"
                        ],
                        "description": "The machine-readable kind. Switch on this rather than on the message."
                      },
                      "message": {
                        "type": "string",
                        "description": "One sentence: what was read, and what to send instead."
                      },
                      "sent": {
                        "type": "number",
                        "description": "The value you sent, as it was parsed."
                      },
                      "didYouMean": {
                        "type": "number",
                        "description": "The value you probably meant — retry with this."
                      }
                    }
                  }
                },
                "meta": {
                  "type": "object",
                  "description": "Provenance: the source site, the matching calculator page, the docs and the disclaimer.",
                  "properties": {
                    "source": {
                      "type": "string"
                    },
                    "calculator": {
                      "type": "string"
                    },
                    "docs": {
                      "type": "string"
                    },
                    "disclaimer": {
                      "type": "string"
                    }
                  }
                }
              },
              "required": [
                "endpoint",
                "inputs",
                "result",
                "meta"
              ]
            }
          }
        }
      },
      "rent-vs-buy-result": {
        "description": "The rent vs buy answer.",
        "headers": {
          "Cache-Control": {
            "description": "Long-lived: the same URL is the same answer forever.",
            "schema": {
              "type": "string"
            }
          },
          "X-RateLimit-Remaining": {
            "description": "Requests left in the current window.",
            "schema": {
              "type": "integer"
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "properties": {
                "endpoint": {
                  "type": "string",
                  "const": "rent-vs-buy"
                },
                "inputs": {
                  "type": "object",
                  "description": "The parsed inputs the answer was computed from: your request with defaults filled in, money parsed out of strings like \"$400,000\", and enum spellings canonicalised.",
                  "properties": {
                    "years": {
                      "type": "integer",
                      "minimum": 1,
                      "maximum": 50,
                      "x-unit": "years",
                      "description": "How long you would stay before selling or moving out."
                    },
                    "homePrice": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 10000000000,
                      "x-unit": "usd",
                      "description": "Purchase price."
                    },
                    "monthlyRent": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 1000000,
                      "x-unit": "usd",
                      "description": "Rent for a comparable place, per month, today."
                    },
                    "downPaymentPercent": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 100,
                      "x-unit": "percent",
                      "description": "Down payment as a percentage of the price. Always echoed; 20 when not sent."
                    },
                    "rate": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 100,
                      "x-unit": "percent",
                      "description": "Mortgage rate (APR). Always echoed; 6.5 when not sent."
                    },
                    "termYears": {
                      "type": "integer",
                      "minimum": 1,
                      "maximum": 50,
                      "x-unit": "years",
                      "description": "Length of the mortgage. Always echoed; 30 when not sent."
                    },
                    "homeGrowth": {
                      "type": "number",
                      "minimum": -20,
                      "maximum": 30,
                      "x-unit": "percent",
                      "description": "Annual home appreciation. May be negative. Always echoed; 4 when not sent."
                    },
                    "rentGrowth": {
                      "type": "number",
                      "minimum": -20,
                      "maximum": 30,
                      "x-unit": "percent",
                      "description": "Annual rent increase, applied on each anniversary. Always echoed; 3 when not sent."
                    },
                    "investReturn": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 30,
                      "x-unit": "percent",
                      "description": "What invested cash earns annually — the down payment's opportunity cost. Always echoed; 7 when not sent."
                    },
                    "propertyTaxPercent": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 10,
                      "x-unit": "percent",
                      "description": "Property tax per year, as a percentage of the home's current value. Always echoed; 1.1 when not sent."
                    },
                    "maintenancePercent": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 10,
                      "x-unit": "percent",
                      "description": "Maintenance per year, as a percentage of the home's current value. Always echoed; 1 when not sent."
                    },
                    "insuranceAnnual": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 1000000,
                      "x-unit": "usd",
                      "description": "Homeowner's insurance per year. Always echoed; 1800 when not sent."
                    },
                    "hoaMonthly": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 100000,
                      "x-unit": "usd",
                      "description": "HOA dues per month. Always echoed; 0 when not sent."
                    },
                    "buyClosingPercent": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 20,
                      "x-unit": "percent",
                      "description": "Buying costs paid up front, as a percentage of the price. Always echoed; 2 when not sent."
                    },
                    "sellClosingPercent": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 20,
                      "x-unit": "percent",
                      "description": "Selling costs, as a percentage of the sale price — agent commission and closing. Always echoed; 6 when not sent."
                    },
                    "pmiRatePct": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 5,
                      "x-unit": "percent",
                      "description": "Annual private mortgage insurance as a percent of the loan, charged only when the down payment leaves the loan above 80% of the price. Defaults to 0.5%, mid-range for the $30–$70 a month per $100,000 borrowed Freddie Mac describes. Send `pmiRatePct=0` for a comparison with no mortgage insurance in it — the right call for a VA loan. Always echoed; 0.5 when not sent."
                    }
                  },
                  "required": [
                    "years",
                    "homePrice",
                    "monthlyRent",
                    "downPaymentPercent",
                    "rate",
                    "termYears",
                    "homeGrowth",
                    "rentGrowth",
                    "investReturn",
                    "propertyTaxPercent",
                    "maintenancePercent",
                    "insuranceAnnual",
                    "hoaMonthly",
                    "buyClosingPercent",
                    "sellClosingPercent",
                    "pmiRatePct"
                  ]
                },
                "result": {
                  "type": "object",
                  "description": "The answer.",
                  "properties": {
                    "verdict": {
                      "description": "\"buy\", \"rent\", or \"toss-up\" when the two land within half a percent of the price.",
                      "type": "string",
                      "enum": [
                        "buy",
                        "rent",
                        "toss-up"
                      ]
                    },
                    "difference": {
                      "description": "Buyer's net worth minus renter's at the end of the stay. Positive means buying won."
                    },
                    "breakEvenYear": {
                      "description": "First year the buyer's net worth catches the renter's. Null when it never happens inside the stay."
                    },
                    "buyerNetWorth": {
                      "description": "Net worth if you buy: today, then one entry per year. Home equity after selling costs, plus investments."
                    },
                    "renterNetWorth": {
                      "description": "Net worth if you rent, over the same points — the invested down payment and every month renting was cheaper."
                    },
                    "monthlyPayment": {
                      "description": "Principal and interest on the mortgage."
                    },
                    "loanAmount": {
                      "description": "The mortgage itself — home price less the down payment."
                    },
                    "remainingBalance": {
                      "description": "What is still owed on that mortgage at the end of the stay."
                    },
                    "years": {
                      "description": "The length of the stay the comparison was run over, echoed back because every other figure is measured at its end."
                    },
                    "finalHomeValue": {
                      "description": "What the home is worth at the end of the stay."
                    },
                    "totals": {
                      "description": "What was spent over the whole stay: rent, mortgage interest, property tax, maintenance, insurance, HOA, and PMI."
                    },
                    "upfront": {
                      "description": "Down payment, buying costs, and the selling costs owed at the end."
                    },
                    "pmi": {
                      "description": "Null when no mortgage insurance is charged. Otherwise the monthly premium, the month it comes off, and what it costs inside the stay."
                    }
                  }
                },
                "notes": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                },
                "warnings": {
                  "type": "array",
                  "description": "Values that were accepted but may not say what you meant. Percentages here are whole numbers, so 6.5 means 6.5%; a percent parameter sent 0.065 is read as 0.065% and warned about rather than silently rescaled. Absent when there is nothing to say.",
                  "items": {
                    "type": "object",
                    "required": [
                      "param",
                      "code",
                      "message",
                      "sent",
                      "didYouMean"
                    ],
                    "properties": {
                      "param": {
                        "type": "string",
                        "description": "The parameter the warning is about."
                      },
                      "code": {
                        "type": "string",
                        "enum": [
                          "percent_looks_like_fraction"
                        ],
                        "description": "The machine-readable kind. Switch on this rather than on the message."
                      },
                      "message": {
                        "type": "string",
                        "description": "One sentence: what was read, and what to send instead."
                      },
                      "sent": {
                        "type": "number",
                        "description": "The value you sent, as it was parsed."
                      },
                      "didYouMean": {
                        "type": "number",
                        "description": "The value you probably meant — retry with this."
                      }
                    }
                  }
                },
                "meta": {
                  "type": "object",
                  "description": "Provenance: the source site, the matching calculator page, the docs and the disclaimer.",
                  "properties": {
                    "source": {
                      "type": "string"
                    },
                    "calculator": {
                      "type": "string"
                    },
                    "docs": {
                      "type": "string"
                    },
                    "disclaimer": {
                      "type": "string"
                    }
                  }
                }
              },
              "required": [
                "endpoint",
                "inputs",
                "result",
                "meta"
              ]
            }
          }
        }
      },
      "rule-of-72-result": {
        "description": "The rule of 72 answer.",
        "headers": {
          "Cache-Control": {
            "description": "Long-lived: the same URL is the same answer forever.",
            "schema": {
              "type": "string"
            }
          },
          "X-RateLimit-Remaining": {
            "description": "Requests left in the current window.",
            "schema": {
              "type": "integer"
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "properties": {
                "endpoint": {
                  "type": "string",
                  "const": "rule-of-72"
                },
                "inputs": {
                  "type": "object",
                  "description": "The parsed inputs the answer was computed from: your request with defaults filled in, money parsed out of strings like \"$400,000\", and enum spellings canonicalised.",
                  "properties": {
                    "rate": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 100,
                      "x-unit": "percent",
                      "description": "Annual rate of return."
                    }
                  },
                  "required": [
                    "rate"
                  ]
                },
                "result": {
                  "type": "object",
                  "description": "The answer.",
                  "properties": {
                    "years": {
                      "description": "72 divided by the rate. Null at or below 0%, where money never doubles."
                    },
                    "exactYears": {
                      "description": "The precise answer, ln(2) / ln(1 + r), for comparison."
                    }
                  }
                },
                "notes": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                },
                "warnings": {
                  "type": "array",
                  "description": "Values that were accepted but may not say what you meant. Percentages here are whole numbers, so 6.5 means 6.5%; a percent parameter sent 0.065 is read as 0.065% and warned about rather than silently rescaled. Absent when there is nothing to say.",
                  "items": {
                    "type": "object",
                    "required": [
                      "param",
                      "code",
                      "message",
                      "sent",
                      "didYouMean"
                    ],
                    "properties": {
                      "param": {
                        "type": "string",
                        "description": "The parameter the warning is about."
                      },
                      "code": {
                        "type": "string",
                        "enum": [
                          "percent_looks_like_fraction"
                        ],
                        "description": "The machine-readable kind. Switch on this rather than on the message."
                      },
                      "message": {
                        "type": "string",
                        "description": "One sentence: what was read, and what to send instead."
                      },
                      "sent": {
                        "type": "number",
                        "description": "The value you sent, as it was parsed."
                      },
                      "didYouMean": {
                        "type": "number",
                        "description": "The value you probably meant — retry with this."
                      }
                    }
                  }
                },
                "meta": {
                  "type": "object",
                  "description": "Provenance: the source site, the matching calculator page, the docs and the disclaimer.",
                  "properties": {
                    "source": {
                      "type": "string"
                    },
                    "calculator": {
                      "type": "string"
                    },
                    "docs": {
                      "type": "string"
                    },
                    "disclaimer": {
                      "type": "string"
                    }
                  }
                }
              },
              "required": [
                "endpoint",
                "inputs",
                "result",
                "meta"
              ]
            }
          }
        }
      },
      "rates-data": {
        "description": "Today's US consumer interest rates.",
        "headers": {
          "Cache-Control": {
            "description": "Good for 21600 seconds — the cadence the source feeds refresh on. Not immortal like an endpoint's answer: this URL carries no inputs, so the same URL genuinely returns a different number when a series publishes.",
            "schema": {
              "type": "string"
            }
          },
          "X-RateLimit-Remaining": {
            "description": "Requests left in the current window.",
            "schema": {
              "type": "integer"
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "description": "The nine national average US interest rates the site runs on, each with its own reading date, its publisher and its change since the last print.",
              "required": [
                "dataset",
                "title",
                "refreshSeconds",
                "rates",
                "caveats",
                "citation",
                "meta"
              ],
              "properties": {
                "dataset": {
                  "type": "string",
                  "const": "rates",
                  "description": "The dataset's slug."
                },
                "title": {
                  "type": "string",
                  "description": "The table's published name."
                },
                "asOf": {
                  "type": "string",
                  "format": "date",
                  "description": "The newest reading in the table — the last time anything here changed, and not the date it was served. Cache on this. Absent only in the degenerate case where no series could be read at all, which also carries a warning."
                },
                "refreshSeconds": {
                  "type": "integer",
                  "description": "How long this answer is good for, matching its Cache-Control and the cadence the feeds are refetched on."
                },
                "rates": {
                  "type": "array",
                  "description": "Every rate that could be read, in the order the page draws them. Nine when the table is whole; a short array carries a warning saying which are missing.",
                  "items": {
                    "type": "object",
                    "description": "One rate: the reading, the date it is for, and the body that published it.",
                    "required": [
                      "id",
                      "label",
                      "description",
                      "value",
                      "unit",
                      "asOf",
                      "asOfLabel",
                      "frequency",
                      "publisher",
                      "publisherUrl",
                      "seriesUrl",
                      "lowerIsBetter",
                      "calculator"
                    ],
                    "properties": {
                      "id": {
                        "type": "string",
                        "description": "The FRED series id, which is also the stable key to store this row under — MORTGAGE30US, DGS10, TERMCBCCINTNS. Labels may be reworded; these will not be renamed."
                      },
                      "label": {
                        "type": "string",
                        "description": "The rate's name as a reader sees it, not the series' formal title."
                      },
                      "description": {
                        "type": "string",
                        "description": "One sentence on what the number measures and who it applies to."
                      },
                      "value": {
                        "type": "number",
                        "description": "The latest reading, as a percentage: 7.03 means 7.03%. Rounded to the two decimals the publisher publishes."
                      },
                      "unit": {
                        "type": "string",
                        "enum": [
                          "percent"
                        ],
                        "description": "What value is denominated in. Every row here is a rate, so this is always percent; it is stated rather than assumed."
                      },
                      "asOf": {
                        "type": "string",
                        "format": "date",
                        "description": "The date this reading is FOR, not the date it was served. Rows differ by months — cite this, and never the date you fetched the answer."
                      },
                      "asOfLabel": {
                        "type": "string",
                        "description": "The same date written out, as the page prints it beside the number — \"Sep 24, 2026\"."
                      },
                      "previous": {
                        "type": "number",
                        "description": "The reading before this one, on the same series and the same scale. Absent when the series had no earlier reading in the window."
                      },
                      "changeFromPrevious": {
                        "type": "number",
                        "description": "value minus previous, in percentage points. Present exactly when previous is. A subtraction of two readings in this answer, not a figure from anywhere else."
                      },
                      "yearAgo": {
                        "type": "number",
                        "description": "The reading closest to twelve months before this one. Absent when the window held nothing that far back."
                      },
                      "changeFromYearAgo": {
                        "type": "number",
                        "description": "value minus yearAgo, in percentage points. Present exactly when yearAgo is."
                      },
                      "frequency": {
                        "type": "string",
                        "description": "FRED's own declared frequency for the series, verbatim — \"Weekly, Ending Thursday\", \"Daily, 7-Day\", \"Monthly\". Note that the three G.19 consumer-credit series are declared Monthly and print a value only in February, May, August and November, so asOf is the field that tells you how old a reading is."
                      },
                      "publisher": {
                        "type": "string",
                        "description": "The body that produced the number — Freddie Mac, the FDIC, the Federal Reserve Board of Governors — rather than FRED, which redistributes it."
                      },
                      "publisherUrl": {
                        "type": "string",
                        "format": "uri",
                        "description": "That body's own page for the series."
                      },
                      "seriesUrl": {
                        "type": "string",
                        "format": "uri",
                        "description": "The FRED series page, where the full history and this reading can be checked."
                      },
                      "lowerIsBetter": {
                        "type": "boolean",
                        "description": "True where a fall is good news for a household (borrowing costs) and false where it is not (savings and CD yields). Stated so that 'rates fell' is never written about a savings average by accident."
                      },
                      "calculator": {
                        "type": "string",
                        "format": "uri",
                        "description": "The Far Better Off calculator that starts from or compares against this reading."
                      }
                    }
                  }
                },
                "caveats": {
                  "type": "array",
                  "description": "What these numbers are not, in plain sentences. Read them before quoting any row.",
                  "items": {
                    "type": "string",
                    "description": "One caveat."
                  }
                },
                "warnings": {
                  "type": "array",
                  "description": "Absent when the table is whole. Present with code missing_series when at least one feed could not be read, so a short array is distinguishable from a table that shrank.",
                  "items": {
                    "type": "object",
                    "description": "One warning.",
                    "required": [
                      "code",
                      "message"
                    ],
                    "properties": {
                      "code": {
                        "type": "string",
                        "enum": [
                          "missing_series"
                        ],
                        "description": "Machine-readable reason."
                      },
                      "message": {
                        "type": "string",
                        "description": "Which series are missing, out of how many, and what that means for this answer."
                      }
                    }
                  }
                },
                "citation": {
                  "type": "string",
                  "description": "A ready-made citation, including the instruction to cite each row's own asOf rather than the fetch date."
                },
                "meta": {
                  "type": "object",
                  "description": "Where this came from and what may be done with it.",
                  "required": [
                    "source",
                    "page",
                    "docs",
                    "license",
                    "attribution",
                    "disclaimer"
                  ],
                  "properties": {
                    "source": {
                      "type": "string",
                      "format": "uri",
                      "description": "The site that published this answer."
                    },
                    "page": {
                      "type": "string",
                      "format": "uri",
                      "description": "The same readings, rendered, with each one's link to its series."
                    },
                    "docs": {
                      "type": "string",
                      "format": "uri",
                      "description": "The API index, on the origin that served this."
                    },
                    "license": {
                      "type": "string",
                      "description": "What may be done with the readings. They are published statistics, with the two Freddie Mac mortgage averages carrying FRED's citation-required condition, which the publisher field satisfies."
                    },
                    "attribution": {
                      "type": "string",
                      "description": "The line to carry if you redistribute these readings."
                    },
                    "disclaimer": {
                      "type": "string",
                      "description": "What this is not."
                    }
                  }
                }
              }
            }
          }
        }
      },
      "income-needed-to-buy-a-house-data": {
        "description": "Income needed to buy the median new home in the United States.",
        "headers": {
          "Cache-Control": {
            "description": "Good for 21600 seconds — the cadence the source feeds refresh on. Not immortal like an endpoint's answer: this URL carries no inputs, so the same URL genuinely returns a different number when a series publishes.",
            "schema": {
              "type": "string"
            }
          },
          "X-RateLimit-Remaining": {
            "description": "Requests left in the current window.",
            "schema": {
              "type": "integer"
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "description": "The income the 28% rule asks for on the median new home, with the payment broken out, the same answer on 10% down, the comparison against median household income, and every source reading behind it.",
              "required": [
                "dataset",
                "title",
                "asOf",
                "refreshSeconds",
                "answer",
                "againstMedianIncome",
                "lowDownPayment",
                "assumptions",
                "sources",
                "method",
                "caveats",
                "citation",
                "meta"
              ],
              "properties": {
                "dataset": {
                  "type": "string",
                  "const": "income-needed-to-buy-a-house",
                  "description": "The dataset's slug."
                },
                "title": {
                  "type": "string",
                  "description": "The figure's published name."
                },
                "asOf": {
                  "type": "string",
                  "format": "date",
                  "description": "The newest of the three source readings — the date this figure last changed, which is not the date it was served. Cache on this rather than on the response time."
                },
                "refreshSeconds": {
                  "type": "integer",
                  "description": "How long this answer is good for, matching its Cache-Control."
                },
                "answer": {
                  "type": "object",
                  "description": "The headline: the median new home on a 20% down payment.",
                  "required": [
                    "price",
                    "ratePct",
                    "termYears",
                    "downPayment",
                    "downPaymentPct",
                    "loanAmount",
                    "principalAndInterest",
                    "propertyTax",
                    "homeownersInsurance",
                    "mortgageInsurance",
                    "monthlyPayment",
                    "annualIncome",
                    "monthlyIncome",
                    "bindingRatio"
                  ],
                  "properties": {
                    "price": {
                      "type": "number",
                      "description": "The median new-home sales price the answer is computed on."
                    },
                    "ratePct": {
                      "type": "number",
                      "description": "The 30-year fixed rate, as a whole number: 6.76 means 6.76%."
                    },
                    "termYears": {
                      "type": "integer",
                      "description": "The loan term the payment is priced over."
                    },
                    "downPayment": {
                      "type": "number",
                      "description": "Cash going in, in dollars."
                    },
                    "downPaymentPct": {
                      "type": "number",
                      "description": "That down payment as a percentage of the price."
                    },
                    "loanAmount": {
                      "type": "number",
                      "description": "What has to be borrowed."
                    },
                    "principalAndInterest": {
                      "type": "number",
                      "description": "The monthly principal-and-interest payment on that loan."
                    },
                    "propertyTax": {
                      "type": "number",
                      "description": "Monthly property tax, at the national average effective rate."
                    },
                    "homeownersInsurance": {
                      "type": "number",
                      "description": "Monthly homeowners premium, from the measured average at this price."
                    },
                    "mortgageInsurance": {
                      "type": "number",
                      "description": "Monthly conventional mortgage insurance. Charged while the loan is over 80% of the price, so it is 0 on the 20%-down answer."
                    },
                    "monthlyPayment": {
                      "type": "number",
                      "description": "The whole housing payment the 28% ratio is measured on."
                    },
                    "annualIncome": {
                      "type": "number",
                      "description": "The gross household income a year the rule asks for at this price."
                    },
                    "monthlyIncome": {
                      "type": "number",
                      "description": "The same income per month, which is what the ratio is written against."
                    },
                    "bindingRatio": {
                      "type": "string",
                      "enum": [
                        "front-end",
                        "back-end"
                      ],
                      "description": "Which of the two ratios sets the answer. With no other debts this is always front-end; the back-end ratio overtakes it when other debts pass two sevenths of the housing payment."
                    }
                  }
                },
                "againstMedianIncome": {
                  "type": "object",
                  "description": "The same figure measured against what households actually earn — three framings, because different readers quote different ones.",
                  "required": [
                    "medianHouseholdIncome",
                    "medianIncomePeriod",
                    "multipleOfMedianIncome",
                    "shareOfMedianIncomePct",
                    "frontEndPct",
                    "incomeGap",
                    "priceAtMedianIncome",
                    "priceGap"
                  ],
                  "properties": {
                    "medianHouseholdIncome": {
                      "type": "number",
                      "description": "Median household income, in the dollars of the year it was earned."
                    },
                    "medianIncomePeriod": {
                      "type": "string",
                      "description": "The year that income reading describes."
                    },
                    "multipleOfMedianIncome": {
                      "type": "number",
                      "description": "Income needed divided by the median household's income."
                    },
                    "shareOfMedianIncomePct": {
                      "type": "number",
                      "description": "The housing payment as a percentage of the median household's gross income. Compare it against frontEndPct."
                    },
                    "frontEndPct": {
                      "type": "number",
                      "description": "The front-end debt-to-income limit the answer is solved against."
                    },
                    "incomeGap": {
                      "type": "number",
                      "description": "Income needed minus the median household's income."
                    },
                    "priceAtMedianIncome": {
                      "type": "number",
                      "description": "The most expensive home the median household's income reaches under the same rule."
                    },
                    "priceGap": {
                      "type": "number",
                      "description": "The median new home's price minus that — the part the median income misses."
                    }
                  }
                },
                "lowDownPayment": {
                  "type": "object",
                  "description": "The same house on half the cash, where conventional mortgage insurance is charged.",
                  "required": [
                    "downPayment",
                    "downPaymentPct",
                    "loanAmount",
                    "principalAndInterest",
                    "propertyTax",
                    "homeownersInsurance",
                    "mortgageInsurance",
                    "monthlyPayment",
                    "annualIncome",
                    "monthlyIncome",
                    "bindingRatio"
                  ],
                  "properties": {
                    "downPayment": {
                      "type": "number",
                      "description": "Cash going in, in dollars."
                    },
                    "downPaymentPct": {
                      "type": "number",
                      "description": "That down payment as a percentage of the price."
                    },
                    "loanAmount": {
                      "type": "number",
                      "description": "What has to be borrowed."
                    },
                    "principalAndInterest": {
                      "type": "number",
                      "description": "The monthly principal-and-interest payment on that loan."
                    },
                    "propertyTax": {
                      "type": "number",
                      "description": "Monthly property tax, at the national average effective rate."
                    },
                    "homeownersInsurance": {
                      "type": "number",
                      "description": "Monthly homeowners premium, from the measured average at this price."
                    },
                    "mortgageInsurance": {
                      "type": "number",
                      "description": "Monthly conventional mortgage insurance. Charged while the loan is over 80% of the price, so it is 0 on the 20%-down answer."
                    },
                    "monthlyPayment": {
                      "type": "number",
                      "description": "The whole housing payment the 28% ratio is measured on."
                    },
                    "annualIncome": {
                      "type": "number",
                      "description": "The gross household income a year the rule asks for at this price."
                    },
                    "monthlyIncome": {
                      "type": "number",
                      "description": "The same income per month, which is what the ratio is written against."
                    },
                    "bindingRatio": {
                      "type": "string",
                      "enum": [
                        "front-end",
                        "back-end"
                      ],
                      "description": "Which of the two ratios sets the answer. With no other debts this is always front-end; the back-end ratio overtakes it when other debts pass two sevenths of the housing payment."
                    }
                  }
                },
                "assumptions": {
                  "type": "object",
                  "description": "Every assumption the arithmetic rests on, stated rather than buried.",
                  "required": [
                    "downPct",
                    "lowDownPct",
                    "termYears",
                    "propertyTaxRatePct",
                    "pmiRatePct",
                    "insuranceAnnual",
                    "frontEndPct",
                    "otherMonthlyDebts"
                  ],
                  "properties": {
                    "downPct": {
                      "type": "number",
                      "description": "The down payment the headline answer assumes, as a percentage."
                    },
                    "lowDownPct": {
                      "type": "number",
                      "description": "The down payment the second answer assumes."
                    },
                    "termYears": {
                      "type": "integer",
                      "description": "Loan term in years."
                    },
                    "propertyTaxRatePct": {
                      "type": "number",
                      "description": "Annual property tax as a percentage of the price."
                    },
                    "pmiRatePct": {
                      "type": "number",
                      "description": "Annual mortgage insurance as a percentage of the loan."
                    },
                    "insuranceAnnual": {
                      "type": "number",
                      "description": "The homeowners premium a year at this price."
                    },
                    "frontEndPct": {
                      "type": "number",
                      "description": "The front-end debt-to-income limit."
                    },
                    "otherMonthlyDebts": {
                      "type": "number",
                      "description": "Other monthly debt payments assumed. Zero, which is why the front-end ratio binds."
                    }
                  }
                },
                "sources": {
                  "type": "array",
                  "description": "The three published series the figure is derived from, each one checkable at its own URL.",
                  "items": {
                    "type": "object",
                    "description": "One source reading.",
                    "required": [
                      "id",
                      "label",
                      "value",
                      "unit",
                      "period",
                      "asOf",
                      "frequency",
                      "publisher",
                      "publisherUrl",
                      "seriesUrl",
                      "live"
                    ],
                    "properties": {
                      "id": {
                        "type": "string",
                        "description": "The FRED series id, which is how a reader looks the input up themselves."
                      },
                      "label": {
                        "type": "string",
                        "description": "The series' own published name."
                      },
                      "value": {
                        "type": "number",
                        "description": "The reading."
                      },
                      "unit": {
                        "type": "string",
                        "enum": [
                          "USD",
                          "percent"
                        ],
                        "description": "What the reading is denominated in."
                      },
                      "period": {
                        "type": "string",
                        "description": "The period the reading is for, written the way that period is named — a day, a quarter or a year."
                      },
                      "asOf": {
                        "type": "string",
                        "format": "date",
                        "description": "The start of that period, as YYYY-MM-DD."
                      },
                      "frequency": {
                        "type": "string",
                        "enum": [
                          "weekly",
                          "quarterly",
                          "annual"
                        ],
                        "description": "How often the publisher updates the series."
                      },
                      "publisher": {
                        "type": "string",
                        "description": "Who produces the number. FRED redistributes it; it does not make it."
                      },
                      "publisherUrl": {
                        "type": "string",
                        "format": "uri",
                        "description": "The publisher's own page for it."
                      },
                      "seriesUrl": {
                        "type": "string",
                        "format": "uri",
                        "description": "The series on FRED."
                      },
                      "live": {
                        "type": "boolean",
                        "description": "False when this feed could not be reached and the dated fallback reading is showing. Stated per source, because one stale series out of three is a different answer from three."
                      }
                    }
                  }
                },
                "method": {
                  "type": "string",
                  "description": "The arithmetic, in a sentence, and the function that performs it."
                },
                "caveats": {
                  "type": "array",
                  "description": "What this figure is not. Read them before quoting it.",
                  "items": {
                    "type": "string",
                    "description": "One caveat, in a sentence."
                  }
                },
                "warnings": {
                  "type": "array",
                  "description": "Present only when a source feed could not be reached. Absent rather than empty when there is nothing to say, the same contract the calculator endpoints use.",
                  "items": {
                    "type": "object",
                    "description": "One warning.",
                    "required": [
                      "code",
                      "message"
                    ],
                    "properties": {
                      "code": {
                        "type": "string",
                        "enum": [
                          "stale_source"
                        ],
                        "description": "Switch on this rather than on the message."
                      },
                      "message": {
                        "type": "string",
                        "description": "One sentence: what is stale and where to check which."
                      }
                    }
                  }
                },
                "citation": {
                  "type": "string",
                  "description": "A copy-ready citation with the price and rate periods baked into it."
                },
                "meta": {
                  "type": "object",
                  "description": "Provenance.",
                  "required": [
                    "source",
                    "page",
                    "docs",
                    "library",
                    "license",
                    "disclaimer"
                  ],
                  "properties": {
                    "source": {
                      "type": "string",
                      "format": "uri",
                      "description": "The site."
                    },
                    "page": {
                      "type": "string",
                      "format": "uri",
                      "description": "The page that shows this figure with its arithmetic in full."
                    },
                    "docs": {
                      "type": "string",
                      "format": "uri",
                      "description": "The API index."
                    },
                    "library": {
                      "type": "string",
                      "description": "The package the arithmetic lives in, so the figure can be re-run rather than trusted."
                    },
                    "license": {
                      "type": "string",
                      "description": "What is licensed and what is not: the arithmetic is MIT, the figure is a fact."
                    },
                    "disclaimer": {
                      "type": "string",
                      "description": "Educational estimate, not financial advice."
                    }
                  }
                }
              }
            }
          }
        }
      },
      "state-income-tax-rates-data": {
        "description": "State income tax on wages, 2026.",
        "headers": {
          "Cache-Control": {
            "description": "Good for 21600 seconds — the cadence the source feeds refresh on. Not immortal like an endpoint's answer: this URL carries no inputs, so the same URL genuinely returns a different number when a series publishes.",
            "schema": {
              "type": "string"
            }
          },
          "X-RateLimit-Remaining": {
            "description": "Requests left in the current window.",
            "schema": {
              "type": "integer"
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "description": "What each of the 51 US states and the District of Columbia charges on wage income in 2026 — a verified rate with its citation, or an explicit refusal that says why there is no number.",
              "required": [
                "dataset",
                "title",
                "taxYear",
                "refreshSeconds",
                "counts",
                "states",
                "caveats",
                "citation",
                "meta"
              ],
              "properties": {
                "dataset": {
                  "type": "string",
                  "const": "state-income-tax-rates",
                  "description": "The dataset's slug."
                },
                "title": {
                  "type": "string",
                  "description": "The table's published name."
                },
                "taxYear": {
                  "type": "integer",
                  "description": "The tax year these rates are in force for. This table's dating field, and deliberately not an asOf: a state legislates a rate for a year, and the day it was transcribed is not what a quoter needs. For whether a newer edition should exist by now, compare meta.dataVintage[].supersededFrom against your own clock."
                },
                "refreshSeconds": {
                  "type": "integer",
                  "description": "How long this answer is good for, matching its Cache-Control."
                },
                "counts": {
                  "type": "object",
                  "description": "The table by case, counted from the rows rather than typed — so a state newly modelled moves these, the page's prose and the rows together.",
                  "required": [
                    "jurisdictions",
                    "noTax",
                    "flat",
                    "graduated",
                    "unmodeled",
                    "cited",
                    "sourceVerified",
                    "sourceQuoted"
                  ],
                  "properties": {
                    "jurisdictions": {
                      "type": "integer",
                      "description": "Rows in the table: 50 states plus the District of Columbia."
                    },
                    "noTax": {
                      "type": "integer",
                      "description": "How many levy no individual income tax on wages at all."
                    },
                    "flat": {
                      "type": "integer",
                      "description": "How many charge one verified statutory rate."
                    },
                    "graduated": {
                      "type": "integer",
                      "description": "How many have a verified bracket schedule."
                    },
                    "unmodeled": {
                      "type": "integer",
                      "description": "How many tax wages on a schedule Far Better Off has not read off the state's own forms."
                    },
                    "cited": {
                      "type": "integer",
                      "description": "How many rows carry a source. Equal to jurisdictions - unmodeled: every row with a rate is cited, and the refusals are the uncited ones."
                    },
                    "sourceVerified": {
                      "type": "integer",
                      "description": "How many of those citations carry a verified day — a page re-opened and found to still say what its row says. The rest have not been re-read since their row was written, which is not the same as doubtful: see the caveat."
                    },
                    "sourceQuoted": {
                      "type": "integer",
                      "description": "How many of those verified citations also carry the page's own sentence in source.quote, which is how many of them you can re-check yourself rather than take on trust. Never more than sourceVerified. The gap is citations in PDFs, which a person read and a substring search cannot repeat."
                    }
                  }
                },
                "states": {
                  "type": "array",
                  "description": "Every jurisdiction, alphabetical by name. Always the whole table — a state with no answer is a row that says so, never a row left out.",
                  "items": {
                    "type": "object",
                    "description": "One jurisdiction: what it charges on wages, or why Far Better Off has no number for it.",
                    "required": [
                      "code",
                      "name",
                      "taxability",
                      "taxesWages",
                      "modeled",
                      "localWageTaxesModeled",
                      "schoolDistrictTaxesModeled",
                      "computeUrl"
                    ],
                    "properties": {
                      "code": {
                        "type": "string",
                        "description": "USPS two-letter code, uppercase. Stable; safe to use as a key."
                      },
                      "name": {
                        "type": "string",
                        "description": "The jurisdiction's full name. D.C. is in this table, so 51 rows, not 50."
                      },
                      "taxability": {
                        "type": "string",
                        "enum": [
                          "none",
                          "flat",
                          "graduated",
                          "unmodeled"
                        ],
                        "description": "Which of four cases this state is, and the field to switch on: \"none\" (taxes no wages at all), \"flat\" (one verified statutory rate), \"graduated\" (a verified bracket schedule, where no single rate describes it) or \"unmodeled\" (a bracket table Far Better Off has not read off the state's own forms, where there is no rate here)."
                      },
                      "taxesWages": {
                        "type": "boolean",
                        "description": "Whether this jurisdiction levies an individual income tax on wages. False in exactly the \"none\" rows — and TRUE in every \"unmodeled\" one, which is the distinction a flattened table loses: an unmodelled state taxes your paycheck, Far Better Off just will not say how much."
                      },
                      "modeled": {
                        "type": "boolean",
                        "description": "Whether Far Better Off can answer for this state. False only where taxability is \"unmodeled\"."
                      },
                      "ratePercent": {
                        "type": "number",
                        "description": "The single statutory rate on taxable income, as a percent: 4.95 means 4.95%. Present only in flat states — a graduated state gets brackets instead, because an average of nine bands is wrong for everybody."
                      },
                      "brackets": {
                        "type": "object",
                        "description": "The state's own published schedule, present only where taxability is \"graduated\". Bands are on taxable income — what is left after the state's deduction — so they line up with the schedule the state prints.",
                        "required": [
                          "year",
                          "schedules"
                        ],
                        "properties": {
                          "year": {
                            "type": "integer",
                            "description": "The year these brackets were published FOR, which is not always taxYear: most states index their bands annually and publish late, and using the last published schedule leaves the answer a little high rather than turning it into a forecast."
                          },
                          "schedules": {
                            "type": "array",
                            "description": "Every distinct ladder the state publishes, with the statuses that share it — collapsed by value, so Virginia's one ladder for all four statuses is one entry rather than four identical ones, and California's three are three.",
                            "items": {
                              "type": "object",
                              "description": "One ladder and who files on it.",
                              "required": [
                                "statuses",
                                "bands"
                              ],
                              "properties": {
                                "statuses": {
                                  "type": "array",
                                  "description": "The filing statuses that share this ladder.",
                                  "items": {
                                    "type": "string",
                                    "enum": [
                                      "single",
                                      "married",
                                      "hoh",
                                      "mfs"
                                    ],
                                    "description": "One filing status."
                                  }
                                },
                                "bands": {
                                  "type": "array",
                                  "description": "The bands, lowest first. The first starts at 0 and the last has no top.",
                                  "items": {
                                    "type": "object",
                                    "description": "One band.",
                                    "required": [
                                      "over",
                                      "ratePercent"
                                    ],
                                    "properties": {
                                      "over": {
                                        "type": "number",
                                        "description": "Taxable income above which this marginal rate applies."
                                      },
                                      "ratePercent": {
                                        "type": "number",
                                        "description": "The marginal rate on income above that, as a percent: 9.3 means 9.3%."
                                      }
                                    }
                                  }
                                }
                              }
                            }
                          }
                        }
                      },
                      "topMarginalRatePercent": {
                        "type": "number",
                        "description": "The rate on one more dollar for a filer already past every threshold in this state's schedule, including any surtax band — the number people mean by \"the top rate\". 0 where taxability is \"none\"; absent where it is \"unmodeled\". Not an average, and not what a typical earner pays."
                      },
                      "standardDeduction": {
                        "type": "object",
                        "description": "What the state exempts before the rate applies, where Far Better Off has verified it. Absent means not yet verified, never verified as zero — for that, read noStandardDeduction.",
                        "required": [
                          "label",
                          "single",
                          "married",
                          "hoh",
                          "mfs"
                        ],
                        "properties": {
                          "label": {
                            "type": "string",
                            "description": "What the state calls it — \"standard deduction\", \"personal exemption\"."
                          },
                          "single": {
                            "type": "number",
                            "description": "The amount for a single filer."
                          },
                          "married": {
                            "type": "number",
                            "description": "The amount for a married filing jointly filer."
                          },
                          "hoh": {
                            "type": "number",
                            "description": "The amount for a head of household filer."
                          },
                          "mfs": {
                            "type": "number",
                            "description": "The amount for a married filing separately filer."
                          }
                        }
                      },
                      "noStandardDeduction": {
                        "type": "boolean",
                        "description": "Present and true only where the state verifiably grants no deduction and no personal exemption at all — Pennsylvania, which says so in as many words. Without this a reader cannot tell its zero from the zero of a state nobody has checked."
                      },
                      "exemptBelow": {
                        "type": "object",
                        "description": "A zero band: taxable income at or below this is not taxed at all, and the rate applies only to what is above it. Given per filing status even where the state writes one figure for everybody.",
                        "required": [
                          "single",
                          "married",
                          "hoh",
                          "mfs"
                        ],
                        "properties": {
                          "single": {
                            "type": "number",
                            "description": "The figure for a single filer."
                          },
                          "married": {
                            "type": "number",
                            "description": "The figure for a married filing jointly filer."
                          },
                          "hoh": {
                            "type": "number",
                            "description": "The figure for a head of household filer."
                          },
                          "mfs": {
                            "type": "number",
                            "description": "The figure for a married filing separately filer."
                          }
                        }
                      },
                      "baseTax": {
                        "type": "number",
                        "description": "Flat dollars owed once the zero band is cleared, before the rate applies to the excess — Ohio's, and nothing else in this table."
                      },
                      "noTaxBelow": {
                        "type": "object",
                        "description": "A filing threshold on income BEFORE the deduction, under which the state charges nothing at all — Virginia's and New Jersey's, and nothing else in this table. Not the same thing as exemptBelow, which is a band inside the schedule.",
                        "required": [
                          "label",
                          "amount",
                          "inclusive"
                        ],
                        "properties": {
                          "label": {
                            "type": "string",
                            "description": "How the state states the threshold."
                          },
                          "amount": {
                            "type": "object",
                            "description": "The threshold, per filing status.",
                            "required": [
                              "single",
                              "married",
                              "hoh",
                              "mfs"
                            ],
                            "properties": {
                              "single": {
                                "type": "number",
                                "description": "The figure for a single filer."
                              },
                              "married": {
                                "type": "number",
                                "description": "The figure for a married filing jointly filer."
                              },
                              "hoh": {
                                "type": "number",
                                "description": "The figure for a head of household filer."
                              },
                              "mfs": {
                                "type": "number",
                                "description": "The figure for a married filing separately filer."
                              }
                            }
                          },
                          "inclusive": {
                            "type": "boolean",
                            "description": "Whether income landing exactly ON the figure is exempt. The two states disagree: New Jersey exempts \"$10,000 or less\" (true), Virginia taxes income that is not \"less than $11,950\" (false)."
                          }
                        }
                      },
                      "surtax": {
                        "type": "object",
                        "description": "A second, higher rate on taxable income above a threshold — Massachusetts's, and nothing else in this table. Its rate is already included in topMarginalRatePercent.",
                        "required": [
                          "ratePercent",
                          "threshold",
                          "label",
                          "year"
                        ],
                        "properties": {
                          "ratePercent": {
                            "type": "number",
                            "description": "The extra rate, in percentage points on top of the main one."
                          },
                          "threshold": {
                            "type": "number",
                            "description": "Taxable income above which the extra rate applies."
                          },
                          "label": {
                            "type": "string",
                            "description": "How the state describes the band."
                          },
                          "year": {
                            "type": "integer",
                            "description": "The year this threshold is in force for; it is indexed annually."
                          }
                        }
                      },
                      "taxesPretaxRetirement": {
                        "type": "boolean",
                        "description": "Whether the state taxes 401(k) and 403(b) elective deferrals as compensation, so the state line is figured on the whole salary. True in Pennsylvania alone. Present for every state with a verified rate, false included, because a missing key here would read as false anyway and that is a claim worth making explicitly."
                      },
                      "adjustments": {
                        "type": "array",
                        "description": "Everything this state's bill does that its headline rate does not say — a tapering credit, a deduction for the FICA you paid, a benefit recapture. Empty where the rate really is the whole rule, which is a claim and not a missing field. Read it before multiplying ratePercent by anything. Each entry carries a kind to switch on as well as the state's own words, so matching a mechanism needs no substring search.",
                        "items": {
                          "type": "object",
                          "description": "One mechanism: a key to match on, the state's own words, and where its figures are.",
                          "required": [
                            "kind",
                            "label"
                          ],
                          "properties": {
                            "kind": {
                              "type": "string",
                              "enum": [
                                "zeroBand",
                                "baseTax",
                                "filingThreshold",
                                "surtax",
                                "credit",
                                "ficaDeduction",
                                "bracketRecapture",
                                "pretaxRetirementTaxed",
                                "noDeduction",
                                "deductionStepsDown",
                                "deductionDisallowed"
                              ],
                              "description": "Which mechanism this is, from a closed set: \"zeroBand\" — Taxable income at or below a threshold is not taxed at all; the rate applies only above it. \"baseTax\" — Flat dollars owed once the zero band is cleared, before the rate applies to the excess. \"filingThreshold\" — Income before the deduction under which the state charges nothing at all. \"surtax\" — A second, higher rate on taxable income above a threshold, already included in topMarginalRatePercent. \"credit\" — A credit against the computed tax, which in these states tapers away as income rises rather than applying flat. \"ficaDeduction\" — The state deducts the Social Security and Medicare tax you paid before applying its own rate. \"bracketRecapture\" — Above a threshold the state takes back the benefit of its lower brackets, so the effective rate exceeds the top band's. \"pretaxRetirementTaxed\" — 401(k) and 403(b) elective deferrals are taxed as compensation, so the state line is figured on the whole salary. \"noDeduction\" — The state grants no standard deduction and no personal exemption, so the rate applies from the first dollar. \"deductionStepsDown\" — The deduction or exemption the state grants shrinks in steps as income rises, so the amount in standardDeduction is the full one and not everybody's — Ohio's personal exemption, which is what standardDeduction holds there. \"deductionDisallowed\" — The deduction or exemption is withdrawn above an income threshold, so the amount in standardDeduction does not apply to a high earner at all."
                            },
                            "label": {
                              "type": "string",
                              "description": "The mechanism in the state's own words, as a breakdown row would show it — worth printing, and not worth matching on, which is what kind is for."
                            },
                            "field": {
                              "type": "string",
                              "description": "The key elsewhere in this same row that carries this mechanism as data: zeroBand → exemptBelow, baseTax → baseTax, filingThreshold → noTaxBelow, surtax → surtax, pretaxRetirementTaxed → taxesPretaxRetirement, noDeduction → noStandardDeduction. ABSENT for the other 5 kinds, which means what it says: this answer names the mechanism and does not quantify it. Send a salary to /api/v1/state-tax for a figure that includes it."
                            }
                          }
                        }
                      },
                      "label": {
                        "type": "string",
                        "description": "How the state describes its own rate, as a breakdown row would show it."
                      },
                      "note": {
                        "type": "string",
                        "description": "What the state figure leaves out, in a sentence meant for a reader."
                      },
                      "reason": {
                        "type": "string",
                        "description": "Present only where taxability is \"unmodeled\": why there is no number, in the same words /api/v1/state-tax gives for the same state."
                      },
                      "localWageTaxesModeled": {
                        "type": "integer",
                        "description": "How many city, county, township or borough wage taxes Far Better Off models in this state. 0 is not a claim that none exist — it is a claim that none are modelled here."
                      },
                      "schoolDistrictTaxesModeled": {
                        "type": "integer",
                        "description": "How many school district income tax levies Far Better Off models in this state. Ohio alone levies these among the states modelled."
                      },
                      "source": {
                        "type": "object",
                        "description": "Where this state's figure was verified — the agency or statute, and a page on its own site. Absent where the rate is confirmed but not yet linked, which is every unmodelled state and a few of the others.",
                        "required": [
                          "publisher",
                          "url"
                        ],
                        "properties": {
                          "publisher": {
                            "type": "string",
                            "description": "The agency or statute, named the way a citation would name it."
                          },
                          "url": {
                            "type": "string",
                            "format": "uri",
                            "description": "A page on that agency's, legislature's or code's own site."
                          },
                          "verified": {
                            "type": "string",
                            "format": "date",
                            "description": "The day this citation was last opened and found to still say what this row says, as an ISO YYYY-MM-DD. This is not taxYear, which is the year the figure is FOR: a legislature sets a rate for a year, not for a day, and that is a different question from when somebody last looked. ABSENT means nobody has re-read it since the row was written — never stale, never verified-as-wrong, and never null. Two of the citations here cannot be re-read by a program at all, and one points at a guide that has not yet published the year this row is for, so a missing verified is not a reason to distrust a rate. What a present one is good for is the opposite direction: it is a day you can repeat."
                          },
                          "quote": {
                            "type": "string",
                            "description": "The sentence on that page which says what this row says — the evidence behind verified, so the reading is repeatable instead of merely asserted. Fetch url, strip its tags to single spaces, fold curly quotes and dashes to ASCII and drop zero-width characters, and this string is still in the text; Far Better Off re-runs exactly that check with `npm run verify-state-sources`. Finding it proves the page still contains the sentence, not that the sentence is still the current rule — Mississippi's page carries 4.4%, 4% and 3.75% at once and only the year in the quote says which is this row's. ABSENT where there is no verified day to evidence, and absent on the two citations that are PDFs (California's and New Jersey's schedules), because no quote should promise a check that cannot run. Never null."
                          }
                        }
                      },
                      "computeUrl": {
                        "type": "string",
                        "format": "uri",
                        "description": "The endpoint that turns this row into a dollar figure, already filled in for this state at an example salary — it applies the deduction, the exempt band, the surtax, the credit and everything else in adjustments."
                      }
                    }
                  }
                },
                "caveats": {
                  "type": "array",
                  "description": "What these numbers are not, in plain sentences. Read them before quoting any row.",
                  "items": {
                    "type": "string",
                    "description": "One caveat."
                  }
                },
                "citation": {
                  "type": "string",
                  "description": "A ready-made citation, including the instruction to cite the tax year rather than the fetch date."
                },
                "meta": {
                  "type": "object",
                  "description": "Where this came from and what may be done with it.",
                  "required": [
                    "source",
                    "page",
                    "docs",
                    "compute",
                    "library",
                    "license",
                    "disclaimer",
                    "dataVintage"
                  ],
                  "properties": {
                    "source": {
                      "type": "string",
                      "format": "uri",
                      "description": "The site that published this answer."
                    },
                    "page": {
                      "type": "string",
                      "format": "uri",
                      "description": "The same table, rendered, with a worked example and every citation clickable."
                    },
                    "docs": {
                      "type": "string",
                      "format": "uri",
                      "description": "The API index, on the origin that served this."
                    },
                    "compute": {
                      "type": "string",
                      "format": "uri",
                      "description": "The endpoint that applies any of these rows to a salary."
                    },
                    "library": {
                      "type": "string",
                      "description": "The package that owns the table and the functions that apply it."
                    },
                    "license": {
                      "type": "string",
                      "description": "What may be done with the table and with the rates."
                    },
                    "disclaimer": {
                      "type": "string",
                      "description": "What this is not."
                    },
                    "dataVintage": {
                      "type": "array",
                      "description": "The dated editions behind this answer — the state rate table and the local wage-tax table, each with the date from which a newer edition should exist.",
                      "items": {
                        "type": "object",
                        "description": "One dated edition.",
                        "required": [
                          "id",
                          "publisher",
                          "edition",
                          "dataYear",
                          "cycleYears",
                          "supersededFrom",
                          "source"
                        ],
                        "properties": {
                          "id": {
                            "type": "string",
                            "description": "Stable identifier for the edition, matching DATA_VINTAGES in @calcwise/finance."
                          },
                          "publisher": {
                            "type": "string",
                            "description": "The body that measured or set these figures."
                          },
                          "edition": {
                            "type": "string",
                            "description": "The specific publication the figures were transcribed from."
                          },
                          "dataYear": {
                            "type": "integer",
                            "description": "The year the figures describe, or are in force for."
                          },
                          "cycleYears": {
                            "type": "integer",
                            "description": "Years between editions: 1 for an annual series, 3 for the triennial SCF."
                          },
                          "supersededFrom": {
                            "type": "string",
                            "format": "date",
                            "description": "The ISO date (UTC) from which a newer edition is reliably published and this one is a year behind. Compare it against your own clock — this response is cacheable, so it carries no live verdict about its own freshness."
                          },
                          "source": {
                            "type": "string",
                            "description": "The page or notice the current figures were read from."
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      },
      "money-numbers-data": {
        "description": "The 2026 money numbers.",
        "headers": {
          "Cache-Control": {
            "description": "Good for 21600 seconds — the cadence the source feeds refresh on. Not immortal like an endpoint's answer: this URL carries no inputs, so the same URL genuinely returns a different number when a series publishes.",
            "schema": {
              "type": "string"
            }
          },
          "X-RateLimit-Remaining": {
            "description": "Requests left in the current window.",
            "schema": {
              "type": "integer"
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "description": "The 2026 US contribution limits, deductions and rates, each with the document that set it: 401(k), IRA and HSA limits, the standard deduction, the Social Security COLA and wage base, and I bond and FDIC figures.",
              "required": [
                "dataset",
                "title",
                "taxYear",
                "asOf",
                "refreshSeconds",
                "figures",
                "sources",
                "caveats",
                "citation",
                "meta"
              ],
              "properties": {
                "dataset": {
                  "type": "string",
                  "const": "money-numbers",
                  "description": "The dataset's slug."
                },
                "title": {
                  "type": "string",
                  "description": "The figure set's published name."
                },
                "taxYear": {
                  "type": "integer",
                  "description": "The tax year these figures are for — the return filed in the following spring."
                },
                "asOf": {
                  "type": "string",
                  "format": "date",
                  "description": "The date every source was last read, which is the date this table last changed. Not the date it was served; cache on this."
                },
                "refreshSeconds": {
                  "type": "integer",
                  "description": "How long this answer is good for, matching its Cache-Control."
                },
                "figures": {
                  "type": "array",
                  "description": "Every figure, in the order the page prints them.",
                  "items": {
                    "type": "object",
                    "description": "One figure: the number, the string the page shows, and the document behind it.",
                    "required": [
                      "id",
                      "label",
                      "section",
                      "sectionTitle",
                      "amount",
                      "unit",
                      "display",
                      "projected",
                      "source"
                    ],
                    "properties": {
                      "id": {
                        "type": "string",
                        "description": "Stable identifier, safe to key on. Labels may be reworded; this will not be renamed once served."
                      },
                      "label": {
                        "type": "string",
                        "description": "The figure's name, as a reader sees it."
                      },
                      "section": {
                        "type": "string",
                        "description": "The section of the page it belongs to, for grouping."
                      },
                      "sectionTitle": {
                        "type": "string",
                        "description": "That section's heading."
                      },
                      "amount": {
                        "type": "number",
                        "description": "The figure as a number, in the unit below."
                      },
                      "unit": {
                        "type": "string",
                        "enum": [
                          "usd",
                          "percent",
                          "years"
                        ],
                        "description": "What amount is denominated in. `percent` is a whole number: 2.8 means 2.8%. `usd` amounts that add to another limit rather than replacing it say so in display, which begins with a plus sign."
                      },
                      "display": {
                        "type": "string",
                        "description": "The same figure as the page prints it, formatted from amount."
                      },
                      "note": {
                        "type": "string",
                        "description": "The qualifying line under the label, where there is one."
                      },
                      "projected": {
                        "type": "boolean",
                        "description": "True on a figure whose official version has not been published yet. Always present, so a projection cannot be mistaken for an official figure by a missing key. Exactly one figure is projected today: the 2027 COLA."
                      },
                      "basis": {
                        "type": "string",
                        "description": "Present on a projected figure: how it was arrived at, with its inputs."
                      },
                      "library": {
                        "type": "string",
                        "description": "Present where @calcwise/finance already owns this number: the export to import instead of scraping it. The site's own calculators run on that constant, so the two cannot diverge."
                      },
                      "source": {
                        "type": "object",
                        "description": "The document a figure was read from — a notice, a revenue procedure, a section of the US Code or a publisher's own page. Never an agency home page.",
                        "required": [
                          "id",
                          "publisher",
                          "document",
                          "url",
                          "retrieved"
                        ],
                        "properties": {
                          "id": {
                            "type": "string",
                            "description": "Stable key for the document, shared by every figure that cites it."
                          },
                          "publisher": {
                            "type": "string",
                            "description": "The body that set or published the figure, not whoever redistributes it."
                          },
                          "document": {
                            "type": "string",
                            "description": "The specific publication, with its notice or section number."
                          },
                          "url": {
                            "type": "string",
                            "format": "uri",
                            "description": "Where it was read."
                          },
                          "retrieved": {
                            "type": "string",
                            "format": "date",
                            "description": "The date this URL was last read end to end for these figures."
                          },
                          "supersededFrom": {
                            "type": "string",
                            "format": "date",
                            "description": "Present where @calcwise/finance tracks this edition: the date from which a newer one is reliably published and this figure is a year behind. Compare it against your own clock."
                          },
                          "vintageId": {
                            "type": "string",
                            "description": "The matching DATA_VINTAGES id in @calcwise/finance, present with supersededFrom."
                          }
                        }
                      }
                    }
                  }
                },
                "sources": {
                  "type": "array",
                  "description": "The distinct documents behind the figures, once each — the bibliography.",
                  "items": {
                    "type": "object",
                    "description": "The document a figure was read from — a notice, a revenue procedure, a section of the US Code or a publisher's own page. Never an agency home page.",
                    "required": [
                      "id",
                      "publisher",
                      "document",
                      "url",
                      "retrieved"
                    ],
                    "properties": {
                      "id": {
                        "type": "string",
                        "description": "Stable key for the document, shared by every figure that cites it."
                      },
                      "publisher": {
                        "type": "string",
                        "description": "The body that set or published the figure, not whoever redistributes it."
                      },
                      "document": {
                        "type": "string",
                        "description": "The specific publication, with its notice or section number."
                      },
                      "url": {
                        "type": "string",
                        "format": "uri",
                        "description": "Where it was read."
                      },
                      "retrieved": {
                        "type": "string",
                        "format": "date",
                        "description": "The date this URL was last read end to end for these figures."
                      },
                      "supersededFrom": {
                        "type": "string",
                        "format": "date",
                        "description": "Present where @calcwise/finance tracks this edition: the date from which a newer one is reliably published and this figure is a year behind. Compare it against your own clock."
                      },
                      "vintageId": {
                        "type": "string",
                        "description": "The matching DATA_VINTAGES id in @calcwise/finance, present with supersededFrom."
                      }
                    }
                  }
                },
                "caveats": {
                  "type": "array",
                  "description": "What these figures are not. Read them before quoting.",
                  "items": {
                    "type": "string",
                    "description": "One caveat, in a sentence."
                  }
                },
                "citation": {
                  "type": "string",
                  "description": "A copy-ready citation with the tax year and review date baked in."
                },
                "meta": {
                  "type": "object",
                  "description": "Provenance.",
                  "required": [
                    "source",
                    "page",
                    "docs",
                    "library",
                    "license",
                    "disclaimer"
                  ],
                  "properties": {
                    "source": {
                      "type": "string",
                      "format": "uri",
                      "description": "The site."
                    },
                    "page": {
                      "type": "string",
                      "format": "uri",
                      "description": "The page that shows these figures with their sources."
                    },
                    "docs": {
                      "type": "string",
                      "format": "uri",
                      "description": "The API index."
                    },
                    "library": {
                      "type": "string",
                      "description": "The package the library-backed figures come from."
                    },
                    "license": {
                      "type": "string",
                      "description": "What is licensed and what is not: the library is MIT, the figures are public facts."
                    },
                    "disclaimer": {
                      "type": "string",
                      "description": "Educational information, not financial or tax advice."
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  }
}
