openapi: 3.1.0
info:
  title: Numeratica — Financial-Planning Calculations API
  version: 1.3.0
  contact:
    name: Numeratica Support
    email: support@numeratica.com
    url: https://numeratica.com
  description: >
    Developer-first API for financial-planning calculations. v1 exposes a
    reproducible Monte Carlo retirement projection plus time-value-of-money,
    goal-funding, and loan-amortization calculators. Spec-first: this document is
    the source of truth for SDK and documentation generation. Every result
    carries an engine_version and a deterministic result_id so runs are
    cacheable, auditable, and reproducible.


    Rate limiting: each API key has a per-minute request budget (token bucket). A
    request over budget gets `429 Too Many Requests` with a `Retry-After` header
    (seconds); retry after that delay. Limits are not enforced in local dev.
servers:
  - url: https://api.numeratica.com
    description: Production
  - url: http://localhost:8080
    description: Local development
security:
  - bearerAuth: []
  - apiKey: []
# Domain tags — one per operation; drives reference grouping and SDK modules.
tags:
  - name: Retirement
    description: "Accumulation, decumulation, RMDs, Roth strategy, and early-withdrawal mechanics."
  - name: Retirement income
    description: "Annuities, pensions, longevity, and survivor income."
  - name: Social Security
    description: "Claiming-age optimization for individuals and couples."
  - name: Tax
    description: "Federal and state income-tax calculation, planning, and optimization."
  - name: Equity compensation
    description: "ISOs, NSOs, RSUs, ESPP, 83(b), NUA, and AMT planning."
  - name: Insurance
    description: "Life, disability, and long-term-care needs analysis."
  - name: Education
    description: "College savings, financial aid, and student-loan repayment."
  - name: Portfolio
    description: "Allocation targets, rebalancing, and capital-market assumptions."
  - name: Healthcare
    description: "ACA subsidies and Medicare costs, before and after 65."
  - name: Utilities
    description: "Foundational money math: TVM, loans, cashflows, limits, and eligibility."
  - name: Platform
    description: "Health, keys, usage, and signup."

paths:
  /readyz:
    get:
      tags: [Platform]
      operationId: getReadyz
      summary: Health / readiness probe
      description: >
        Public readiness check — verifies the service can reach its dependencies
        (the database). Returns 503 if not. (`/healthz` is a liveness-only probe and
        can be intercepted by some network proxies; `/readyz` is the reliable one.)
      security: [] # public; no API key required
      responses:
        "200":
          description: Service is up and dependencies are reachable
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, example: ok }
        "503":
          description: A dependency (the database) is unreachable
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, example: unavailable }
                  reason: { type: string, example: database }
  /v1/retirement/monte-carlo:
    post:
      tags: [Retirement]
      operationId: retirementMonteCarlo
      summary: Run a Monte Carlo retirement projection
      description: >
        Simulates many market paths to estimate the probability that a plan's
        money lasts to the planning horizon, plus the distribution of ending
        balances. Deterministic: the same inputs and seed always return the same
        result and result_id.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/MonteCarloRequest"
            examples:
              default:
                value:
                  current_age: 40
                  retirement_age: 65
                  end_age: 95
                  current_balance: 500000
                  annual_contribution: 30000
                  annual_spending: 60000
                  expected_return: 0.07
                  return_volatility: 0.12
                  inflation: 0.025
                  num_simulations: 10000
                  seed: 12345
              guyton_klinger:
                summary: Dynamic spending with Guyton-Klinger guardrails
                value:
                  current_age: 65
                  retirement_age: 65
                  end_age: 95
                  current_balance: 1000000
                  annual_contribution: 0
                  annual_spending: 55000
                  expected_return: 0.05
                  return_volatility: 0.16
                  inflation: 0.025
                  num_simulations: 10000
                  seed: 12345
                  spending_rule:
                    type: guyton_klinger
      responses:
        "200":
          description: Projection result
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MonteCarloResponse"
        "400":
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
        "405":
          description: Method not allowed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /v1/tvm:
    post:
      tags: [Utilities]
      operationId: tvm
      summary: Solve a time-value-of-money problem
      description: >
        Relates the five standard variables (rate, nper, pmt, pv, fv) and solves
        for the one named in solve_for given the other four. Sign convention:
        cash you pay out is negative, cash you receive is positive. rate is the
        interest rate per period; nper is the number of periods. Deterministic.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/TVMRequest"
            examples:
              loanPayment:
                summary: Monthly payment on a 200k loan, 4% APR, 30 years
                value:
                  solve_for: pmt
                  rate: 0.0033333333
                  nper: 360
                  pv: -200000
                  fv: 0
      responses:
        "200":
          description: Solved variables
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TVMResponse"
        "400":
          description: Invalid request or no solution
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
        "405":
          description: Method not allowed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /v1/goal-funding:
    post:
      tags: [Utilities]
      operationId: goalFunding
      summary: Compute the contribution required to reach a savings goal
      description: >
        Given a target amount, a horizon, an expected return, and any current
        savings, computes the periodic contribution needed to reach the goal.
        Deterministic.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/GoalFundingRequest"
            examples:
              default:
                value:
                  target_amount: 1000000
                  current_savings: 50000
                  years: 30
                  annual_return: 0.06
                  periods_per_year: 12
      responses:
        "200":
          description: Funding plan
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/GoalFundingResponse"
        "400":
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
        "405":
          description: Method not allowed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /v1/loan/amortization:
    post:
      tags: [Utilities]
      operationId: loanAmortization
      summary: Amortize a fixed-rate loan
      description: >
        Computes the level monthly payment, total interest, payoff time, and an
        optional per-period amortization schedule. Supports an optional extra
        monthly principal payment, which shortens the term. Deterministic.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/LoanRequest"
            examples:
              mortgage:
                value:
                  principal: 300000
                  annual_rate: 0.065
                  term_months: 360
                  include_schedule: false
      responses:
        "200":
          description: Amortization result
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LoanResponse"
        "400":
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
        "405":
          description: Method not allowed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /v1/tax/estimate:
    post:
      tags: [Tax]
      operationId: taxEstimate
      summary: Estimate U.S. federal income tax
      description: >
        Estimates federal income tax for planning purposes: ordinary-income
        brackets, the standard deduction (with optional itemized override),
        long-term capital gains / qualified dividends stacked on top of ordinary
        income, the 3.8% Net Investment Income Tax, the 0.9% Additional Medicare
        Tax on earned income, and an optional flat state rate. Calculation engine
        output, not tax advice — credits, AMT, phase-outs, QBI, and
        state-specific rules beyond a flat rate are not modeled. Deterministic.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/TaxEstimateRequest"
            examples:
              default:
                value:
                  tax_year: 2025
                  filing_status: single
                  ordinary_income: 120000
                  qualified_income: 15000
                  state_tax_rate: 0.05
      responses:
        "200":
          description: Tax estimate
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TaxEstimateResponse"
        "400":
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
        "405":
          description: Method not allowed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /v1/retirement/rmd:
    post:
      tags: [Retirement]
      operationId: retirementRmd
      summary: Compute a Required Minimum Distribution
      description: >
        Computes the Required Minimum Distribution from tax-deferred retirement
        accounts: the prior-year-end balance divided by the IRS Uniform Lifetime
        Table distribution period for the owner's age. The beginning age follows
        SECURE 2.0 (73, or 75 for those born 1960 or later). Uses the Uniform
        Lifetime Table only; the Joint Life and Single Life tables are not
        modeled. Calculation engine output, not tax advice. Deterministic.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/RMDRequest"
            examples:
              default:
                value:
                  age: 73
                  account_balance: 1000000
                  birth_year: 1953
      responses:
        "200":
          description: RMD result
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RMDResponse"
        "400":
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
        "405":
          description: Method not allowed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /v1/retirement/withdrawal-sequencing:
    post:
      tags: [Retirement]
      operationId: retirementWithdrawalSequencing
      summary: Plan a year of tax-aware retirement withdrawals
      description: >
        Decides how much to withdraw from taxable, tax-deferred, and tax-free
        (Roth) accounts to net a target after-tax spending amount, accounting for
        the tax the withdrawals themselves generate. Supports three strategies
        (conventional, bracket_fill, proportional), models taxable cost basis so
        only the gain is taxed, and — when an age is supplied — takes the year's
        RMD from the tax-deferred account first. Calculation engine output, not
        tax advice. Deterministic.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/WithdrawalSequencingRequest"
            examples:
              bracketFill:
                value:
                  tax_year: 2025
                  filing_status: mfj
                  strategy: bracket_fill
                  bracket_ceiling: 0.22
                  spending_need: 90000
                  taxable_balance: 300000
                  tax_deferred_balance: 800000
                  tax_free_balance: 250000
                  taxable_cost_basis_fraction: 0.7
                  other_ordinary_income: 30000
                  age: 73
                  birth_year: 1953
      responses:
        "200":
          description: Withdrawal plan
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WithdrawalSequencingResponse"
        "400":
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
        "405":
          description: Method not allowed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /v1/retirement/withdrawal-strategy:
    post:
      tags: [Retirement]
      operationId: retirementWithdrawalStrategy
      summary: Simulate a dynamic retirement-spending strategy
      description: >
        Projects retirement spending under a chosen dynamic-withdrawal rule
        across Monte Carlo market paths, reporting the year-by-year spending
        distribution, success probability, spending volatility, and ending-balance
        percentiles. Strategies: fixed_real (constant real, the "4% rule"),
        fixed_pct (percent of current balance), guardrails (Guyton-Klinger), vpw
        (Variable Percentage Withdrawal), and floor_and_ceiling. Shares the Monte
        Carlo return model and deterministic RNG. Calculation engine output, not
        financial advice. Deterministic given a seed.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/WithdrawalStrategyRequest"
            examples:
              guardrails:
                value:
                  current_age: 65
                  end_age: 95
                  current_balance: 1000000
                  initial_rate: 0.04
                  strategy: guardrails
                  expected_return: 0.05
                  return_volatility: 0.12
                  inflation: 0.02
                  seed: 42
      responses:
        "200":
          description: Dynamic-withdrawal projection
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WithdrawalStrategyResponse"
        "400":
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
        "405":
          description: Method not allowed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /v1/roth-conversion:
    post:
      tags: [Retirement]
      operationId: rothConversion
      summary: Project a multi-year Roth conversion strategy
      description: >
        Projects converting tax-deferred savings to a Roth account each year up to
        the top of a target ordinary-income bracket, against a no-conversion
        baseline. Reuses the tax and RMD engines. Assumptions (v1): real (today's)
        dollars with a real return and the chosen year's brackets held constant;
        conversion tax paid from the converted amount; RMDs begin at the SECURE
        2.0 age, are taken before conversions, and are assumed spent (not
        reinvested). Calculation engine output, not tax advice. Deterministic.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/RothConversionRequest"
            examples:
              gapYears:
                value:
                  tax_year: 2026
                  filing_status: mfj
                  current_age: 63
                  end_age: 90
                  birth_year: 1963
                  tax_deferred_balance: 1500000
                  roth_balance: 100000
                  other_ordinary_income: 30000
                  annual_return: 0.05
                  target_bracket: 0.24
                  conversion_end_age: 73
      responses:
        "200":
          description: Conversion projection
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RothConversionResponse"
        "400":
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
        "405":
          description: Method not allowed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /v1/tax/state-income:
    post:
      tags: [Tax]
      operationId: taxStateIncome
      summary: State income tax (all 50 states + DC)
      description: >
        Computes state income tax for all 50 states + DC: the nine no-income-tax
        states ($0), the flat-rate states (single rate), and the graduated states
        with full brackets and state standard deductions (incl. the CA 1% MHSA
        and MA 4% millionaire surtaxes, and 0%% floors in ND/SC/DE).
        Complements the federal tax/estimate engine. v1 covers tax year 2025.
        Optional pension_income (generic qualified pension/401k/403b/IRA) applies
        each state's 2025 retirement-income exclusion — full exemption, fixed
        dollar caps, AGI phase-outs, or NJ/CT tiers — gated by age. Social Security
        is handled separately. Does not model flat states' own standard deductions.
        Deterministic.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/StateTaxRequest"
            examples:
              california:
                value:
                  state: CA
                  tax_year: 2025
                  filing_status: single
                  state_taxable_income: 100000
              georgia_retiree:
                summary: Georgia retiree — $50k pension fully excluded at age 67
                value:
                  state: GA
                  tax_year: 2025
                  filing_status: single
                  state_taxable_income: 20000
                  social_security_income: 25000
                  pension_income: 50000
                  age: 67
      responses:
        "200":
          description: State tax
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/StateTaxResponse"
        "400":
          description: Invalid request (incl. uncovered state)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
        "405":
          description: Method not allowed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /v1/medicare/irmaa:
    post:
      tags: [Healthcare]
      operationId: medicareIrmaa
      summary: Medicare IRMAA Part B & Part D surcharges
      description: >
        Computes the Medicare income-related monthly adjustment amount (IRMAA) —
        the Part B and Part D surcharges high-income beneficiaries pay on top of
        the standard premiums — from the benefit year, filing status, and MAGI.
        IRMAA uses MAGI from two years before the benefit year (a cliff: $1 over a
        threshold triggers the full surcharge). Returns the tier, monthly and
        annual surcharge, and the headroom to the next tier (which drives Roth /
        withdrawal decisions). Year-keyed SSA/CMS tables. Deterministic.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/IRMAARequest"
            examples:
              default:
                value:
                  irmaa_year: 2026
                  filing_status: mfj
                  magi: 280000
      responses:
        "200":
          description: IRMAA result
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IRMAAResponse"
        "400":
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
        "405":
          description: Method not allowed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /v1/medicare/late-enrollment-penalty:
    post:
      tags: [Healthcare]
      operationId: medicareLateEnrollmentPenalty
      summary: Medicare Part B & Part D late-enrollment penalties
      description: >
        Computes the permanent Medicare late-enrollment penalties (LEP) for delaying Part B and/or
        Part D without qualifying coverage. Part B adds 10% of the standard Part B premium for each
        full 12-month period late; Part D adds 1% of the national base beneficiary premium for each
        full month without creditable drug coverage — both rounded to the nearest $0.10 and paid for
        life. Returns the monthly, annual, and projected lifetime surcharge. Year-keyed CMS/SSA
        figures. Deterministic; confirm determinations with Medicare/SSA.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/MedicareLEPRequest"
            examples:
              both_parts_late:
                summary: 2026, 30 months late on Part B (2 periods) and 30 months without Part D coverage
                value:
                  year: 2026
                  part_b_months_late: 30
                  part_d_months_late: 30
                  projection_years: 20
      responses:
        "200":
          description: The penalty breakdown
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MedicareLEPResponse"
        "400":
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
        "405":
          description: Method not allowed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /v1/tax/bracket-room:
    post:
      tags: [Tax]
      operationId: taxBracketRoom
      summary: Room to the top of each tax bracket
      description: >
        Reports how much more ordinary income fits before crossing into each
        higher federal bracket (room to the top of the current and each higher
        bracket), plus the room remaining in the 0% and 15% long-term capital
        gains brackets. Powers bracket-fill decisions (Roth conversions, withdrawal
        sequencing, gain harvesting). Uses the year-keyed federal tables.
        Deterministic.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/BracketRoomRequest"
            examples:
              default:
                value:
                  tax_year: 2026
                  filing_status: mfj
                  taxable_income: 120000
      responses:
        "200":
          description: Bracket room
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/BracketRoomResponse"
        "400":
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
        "405":
          description: Method not allowed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /v1/social-security:
    post:
      tags: [Social Security]
      operationId: socialSecurity
      summary: Optimize the Social Security claiming age
      description: >
        Given the Primary Insurance Amount (PIA, the monthly benefit at Full
        Retirement Age) and birth year, computes the benefit at each claiming age
        62–70 (applying the SSA early-claim reductions and delayed-retirement
        credits) and the age that maximizes lifetime benefits to a chosen
        longevity, plus the 62-vs-70 breakeven age. Amounts are real (today's)
        dollars; discount_rate is a real rate and present values discount to age
        62. Calculation engine output, not financial advice. Deterministic.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/SocialSecurityRequest"
            examples:
              default:
                value:
                  birth_year: 1962
                  pia_monthly: 2800
                  longevity_age: 92
                  discount_rate: 0.02
      responses:
        "200":
          description: Claiming analysis
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SocialSecurityResponse"
        "400":
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
        "405":
          description: Method not allowed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /v1/healthcare-costs:
    post:
      tags: [Healthcare]
      operationId: healthcareCosts
      summary: Retirement health-insurance costs (ACA subsidy / Medicare)
      description: >
        Before age 65, estimates the ACA premium subsidy as a function of MAGI —
        the hidden cost of Roth conversions and withdrawals that raise income,
        including the 400%-of-FPL subsidy cliff that returns in 2026. At 65+,
        returns the Medicare Part B cost (base + IRMAA). The benchmark premium
        (second-lowest Silver plan) is an input (area/age specific). 48 states +
        DC. Year-keyed FPL + applicable-percentage tables. Deterministic.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/HealthcareRequest"
            examples:
              preMedicare:
                value:
                  coverage_year: 2026
                  household_size: 2
                  magi: 70000
                  benchmark_annual_premium: 16000
                  age: 60
      responses:
        "200":
          description: Cost estimate
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/HealthcareResponse"
        "400":
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
        "405":
          description: Method not allowed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /v1/annuity:
    post:
      tags: [Retirement income]
      operationId: annuity
      summary: Price an income annuity (SPIA / QLAC) on the mortality table
      description: >
        Given a premium, solves the annual/monthly income the annuity buys (or,
        given a target income, the premium required), with the expected total
        payout and a money's-worth ratio. Supports immediate or deferred (QLAC
        start_age), and life / period-certain / joint-and-survivor payouts.
        Priced as the survival-weighted present value on the SSA 2021 mortality
        table at the assumed rate. Deterministic; not an insurance quote.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/AnnuityRequest"
            examples:
              spia:
                value:
                  premium: 100000
                  age: 65
                  sex: male
                  assumed_rate: 0.045
                  payout_type: life
      responses:
        "200":
          description: Annuity pricing
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AnnuityResponse"
        "400":
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
        "405":
          description: Method not allowed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /v1/longevity:
    post:
      tags: [Retirement income]
      operationId: longevity
      summary: Life expectancy & survival probabilities (SSA period life table)
      description: >
        Life expectancy, the survival-probability curve, milestone probabilities
        (reaching 80/85/90/95/100), and the median age at death — from the SSA
        2021 period life table. With a second life supplied, also returns the
        joint (first-death) and last-survivor life expectancies. A frailty
        multiplier scales the death probabilities. Deterministic.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/LongevityRequest"
            examples:
              couple:
                value:
                  age: 65
                  sex: male
                  second_age: 63
                  second_sex: female
      responses:
        "200":
          description: Longevity analysis
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LongevityResponse"
        "400":
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
        "405":
          description: Method not allowed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /v1/retirement/survivor-scenario:
    post:
      tags: [Retirement income]
      operationId: retirementSurvivorScenario
      summary: Income & tax at the first spouse's death
      description: >
        Models the survivor's cliff: the surviving spouse keeps the larger Social
        Security benefit (the smaller stops — the SS survivor step-down), loses
        the deceased's own income, and switches from MFJ to single filing, whose
        narrower brackets tax the remaining income harder (bracket compression).
        Composes the tax and (optional) IRMAA engines. Deterministic.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/SurvivorRequest"
            examples:
              default:
                value:
                  tax_year: 2025
                  spouse_a: { social_security: 24000, other_income: 0 }
                  spouse_b: { social_security: 36000, other_income: 0 }
                  shared_ordinary_income: 80000
      responses:
        "200":
          description: Survivor comparison
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SurvivorResponse"
        "400":
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
        "405":
          description: Method not allowed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /v1/pension/lump-sum-vs-annuity:
    post:
      tags: [Retirement income]
      operationId: pensionLumpSumVsAnnuity
      summary: Compare a lump-sum pension offer to the monthly annuity
      description: >
        Values the (optionally COLA-adjusted, optionally joint-and-survivor)
        pension stream at a discount rate, computes the implied return of taking
        the annuity versus the lump sum, and finds the nominal breakeven age.
        Reuses the npv-irr engine for the PV and internal-rate math. The
        longevity assumption is an input. Deterministic.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/PensionRequest"
            examples:
              default:
                value:
                  current_age: 65
                  lump_sum_offer: 300000
                  monthly_pension: 2000
                  discount_rate: 0.05
                  end_age: 90
      responses:
        "200":
          description: Comparison
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PensionResponse"
        "400":
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
        "405":
          description: Method not allowed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /v1/retirement/accumulation-projection:
    post:
      tags: [Retirement]
      operationId: retirementAccumulationProjection
      summary: Project savings to retirement across account types
      description: >
        Deterministic accumulation across multiple accounts with contribution
        escalation and an employer match, returning the projected balance by
        account and total (nominal + real). With num_simulations > 0 it reuses
        the Monte Carlo return model to add stochastic ending-total percentile
        bands. Deterministic given a seed.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/AccumulationRequest"
            examples:
              default:
                value:
                  accounts:
                    - { name: "401k", current_balance: 150000, annual_contribution: 20000, receives_match: true }
                    - { name: "ira", current_balance: 50000, annual_contribution: 7000 }
                  escalation_rate: 0.03
                  employer_match_rate: 0.5
                  employer_match_limit: 6000
                  expected_return: 0.06
                  return_volatility: 0.15
                  inflation: 0.025
                  years: 30
                  num_simulations: 2000
                  seed: 11
      responses:
        "200":
          description: Accumulation projection
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AccumulationResponse"
        "400":
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
        "405":
          description: Method not allowed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /v1/inherited-ira:
    post:
      tags: [Retirement]
      operationId: inheritedIra
      summary: Optimize an inherited-IRA 10-year drawdown (SECURE Act)
      description: >
        Compares distribution schedules for a non-spouse inherited traditional IRA
        that must be emptied within 10 years, and recommends the one that leaves
        the heir with the most after-tax wealth. Because the account is fully
        distributed and proceeds are reinvested at the same rate, terminal wealth
        = grossFV − future-value-of-taxes, so the optimizer minimizes the FV of
        taxes — trading off deferral against bracket-spiking. Strategies: even,
        bracket_fill (up to the target bracket), defer, immediate. Composes the
        federal tax engine. Brackets held constant across the window; supply
        annual_rmd_floor when the decedent had begun RMDs. Deterministic.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/InheritedIRARequest"
            examples:
              default:
                value:
                  inherited_balance: 1000000
                  account_growth_rate: 0.06
                  tax_year: 2025
                  filing_status: single
                  other_income: 80000
                  target_bracket: 0.24
      responses:
        "200":
          description: Drawdown comparison
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/InheritedIRAResponse"
        "400":
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
        "405":
          description: Method not allowed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /v1/debt-vs-invest:
    post:
      tags: [Utilities]
      operationId: debtVsInvest
      summary: Pay down debt vs. invest the extra (paired Monte Carlo)
      description: >
        Holds the monthly budget constant and compares two strategies over a
        horizon: throw the whole budget at the debt then invest the freed budget
        (pay_debt), or pay only the minimum and invest the extra (invest). Both
        see the same market path each month, so the difference reflects strategy,
        not luck. The guaranteed hurdle is the debt's after-tax rate (reduced by
        the marginal rate when the interest is deductible). Returns each
        strategy's net-worth distribution, the probability investing wins, and a
        recommendation. Deterministic given a seed.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/DebtInvestRequest"
            examples:
              creditCard:
                value:
                  loan_balance: 30000
                  loan_annual_rate: 0.18
                  loan_minimum_payment: 350
                  extra_monthly: 500
                  horizon_years: 15
                  expected_return: 0.06
                  return_volatility: 0.15
      responses:
        "200":
          description: Strategy comparison
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DebtInvestResponse"
        "400":
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
        "405":
          description: Method not allowed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /v1/college-savings:
    post:
      tags: [Education]
      operationId: collegeSavings
      summary: Size a 529 / college-savings goal (tuition-inflated)
      description: >
        Projects the future cost of college using a tuition-specific inflation
        rate (tuition historically outpaces CPI), computes the savings needed by
        the time college starts, and solves for the periodic contribution required
        from today — the goal-funding math for the accumulate-then-draw-down shape
        of an education goal. Deterministic.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CollegeRequest"
            examples:
              default:
                value:
                  current_child_age: 5
                  annual_cost_today: 30000
                  tuition_inflation: 0.05
                  current_savings: 20000
                  expected_return: 0.06
      responses:
        "200":
          description: Funding analysis
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CollegeResponse"
        "400":
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
        "405":
          description: Method not allowed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /v1/retirement/savings-target:
    post:
      tags: [Retirement]
      operationId: retirementSavingsTarget
      summary: Required savings to fund a target retirement income
      description: >
        The inverse of an accumulation projection: given a target retirement
        income, other income (SS/pension), and return/inflation assumptions,
        returns the required nest egg, the annual/monthly saving needed from
        today, and the gap versus the current trajectory. Works in real (today's)
        dollars. For a success-probability target, pair with the Monte Carlo
        endpoint. Deterministic.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/SavingsTargetRequest"
            examples:
              default:
                value:
                  current_age: 40
                  retirement_age: 65
                  end_age: 90
                  target_annual_income: 80000
                  other_income: 30000
                  current_savings: 200000
                  expected_return: 0.06
                  inflation: 0.025
                  withdrawal_rate: 0.04
      responses:
        "200":
          description: Savings analysis
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SavingsTargetResponse"
        "400":
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
        "405":
          description: Method not allowed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /v1/retirement/hsa:
    post:
      tags: [Retirement]
      operationId: retirementHsa
      summary: HSA contribution limit + long-horizon projection
      description: >
        Returns the HSA contribution limit (with the last-month rule and
        month-based proration) and projects the balance over a horizon, with an
        optional triple-tax-advantage illustration versus a taxable account.
        Reuses the contribution-limits engine for the year-keyed HSA figures.
        Deterministic.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/HSARequest"
            examples:
              default:
                value:
                  tax_year: 2026
                  coverage: family
                  age: 55
                  annual_contribution: 8000
                  expected_return: 0.06
                  years: 20
                  marginal_rate: 0.24
      responses:
        "200":
          description: Limit + projection
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/HSAResponse"
        "400":
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
        "405":
          description: Method not allowed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /v1/inflation-adjust:
    post:
      tags: [Utilities]
      operationId: inflationAdjust
      summary: Convert between real and nominal dollars (COLA helper)
      description: >
        Inflates a present amount to a future nominal value (to_nominal) or
        deflates a future amount to today's purchasing power (to_real) at a
        constant annual rate, and returns the year-by-year stream. The rate is an
        input (no CPI table is embedded). Deterministic.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/InflationAdjustRequest"
            examples:
              default:
                value:
                  amount: 100
                  annual_rate: 0.025
                  years: 10
                  direction: to_nominal
      responses:
        "200":
          description: Adjusted amount + schedule
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/InflationAdjustResponse"
        "400":
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
        "405":
          description: Method not allowed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /v1/cashflow/npv-irr:
    post:
      tags: [Utilities]
      operationId: cashflowNpvIrr
      summary: NPV / IRR over periodic or dated cashflows
      description: >
        Computes net present value at a given rate and the internal rate of
        return for a cashflow series — either periodic (NPV/IRR, one flow per
        period) or dated (XNPV/XIRR, actual/365 day-count). IRR is a bounded
        root-find (sign-change scan + bisection with a fixed iteration cap); it is
        omitted when the cashflows never change sign. Provide exactly one of
        cashflows or dated_cashflows. Deterministic.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/NPVIRRRequest"
            examples:
              periodic:
                value:
                  rate: 0.10
                  cashflows: [-1000, 500, 400, 300, 100]
              dated:
                value:
                  rate: 0.08
                  dated_cashflows:
                    - { date: "2025-01-01", amount: -10000 }
                    - { date: "2025-09-15", amount: 3000 }
                    - { date: "2026-06-30", amount: 9000 }
      responses:
        "200":
          description: NPV/IRR result
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/NPVIRRResponse"
        "400":
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
        "405":
          description: Method not allowed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /v1/contribution-limits:
    post:
      tags: [Utilities]
      operationId: contributionLimits
      summary: IRS retirement & HSA contribution limits for a person/year
      description: >
        Returns the contribution limits that apply to a given person in a given
        year: the §402(g) elective-deferral limit and the age-appropriate
        catch-up (auto-selecting the SECURE 2.0 ages-60-63 super catch-up), the
        IRA limit, the §415(c) annual-additions ceiling, SIMPLE and SEP limits,
        and the HSA limit — each capped at compensation where applicable. Also
        flags the SECURE 2.0 §603 Roth catch-up mandate. Year-keyed IRS tables.
        Deterministic.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ContributionLimitsRequest"
            examples:
              default:
                value:
                  tax_year: 2026
                  age: 62
                  compensation: 250000
                  prior_year_wages: 250000
                  hsa_coverage: family
      responses:
        "200":
          description: Applicable contribution limits
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ContributionLimitsResponse"
        "400":
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
        "405":
          description: Method not allowed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /v1/ira-eligibility:
    post:
      tags: [Utilities]
      operationId: iraEligibility
      summary: IRA deductibility & Roth contribution eligibility by MAGI
      description: >
        Determines how much of an IRA contribution is tax-deductible
        (traditional) and how much can be contributed to a Roth, by applying the
        IRS MAGI phase-out ranges for the filing status. Traditional deductibility
        depends on whether the contributor (or spouse) is an active participant in
        a workplace plan. Reuses the contribution-limits engine for the IRA dollar
        figure. Year-keyed IRS tables. Deterministic.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/IRAEligibilityRequest"
            examples:
              coveredSingle:
                value:
                  tax_year: 2026
                  filing_status: single
                  magi: 160000
                  age: 50
                  covered_by_workplace_plan: true
      responses:
        "200":
          description: IRA eligibility
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IRAEligibilityResponse"
        "400":
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
        "405":
          description: Method not allowed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /v1/capital-market-assumptions:
    post:
      tags: [Portfolio]
      operationId: capitalMarketAssumptions
      summary: Blend asset-class assumptions into a portfolio return & volatility
      description: >
        Combines per-asset-class capital-market assumptions (expected return,
        volatility, weight) and a correlation structure into a single portfolio
        expected return and volatility via mean/variance (Markowitz) aggregation.
        The outputs map directly to the Monte Carlo endpoint's expected_return and
        return_volatility. Deterministic.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CMARequest"
            examples:
              sixtyForty:
                value:
                  assets:
                    - { name: stocks, expected_return: 0.08, volatility: 0.18, weight: 0.6 }
                    - { name: bonds, expected_return: 0.03, volatility: 0.06, weight: 0.4 }
                  default_correlation: 0.1
      responses:
        "200":
          description: Blended portfolio
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CMAResponse"
        "400":
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
        "405":
          description: Method not allowed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /v1/equity-comp/iso:
    post:
      tags: [Equity compensation]
      operationId: equityCompIso
      summary: ISO exercise → AMT impact (§55, via the tax engine)
      description: >
        Computes the Alternative Minimum Tax impact of exercising and holding
        incentive stock options. The bargain element ((fmv − strike) × shares) is an
        AMT preference; it is routed through the tax engine's §55 path (run with and
        without the preference) so the incremental AMT — which generally becomes a
        Minimum Tax Credit (§53) in later years — reflects the current exemption and
        phase-out. Held past year-end (no regular-tax income / no disqualifying
        disposition). Deterministic; not tax advice.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/EquityCompISORequest"
            examples:
              iso:
                value:
                  tax_year: 2025
                  filing_status: single
                  ordinary_income: 200000
                  shares: 10000
                  strike_price: 1
                  fmv_at_exercise: 11
      responses:
        "200":
          description: ISO AMT impact
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EquityCompISOResponse"
        "400":
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
        "405":
          description: Method not allowed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /v1/equity-comp/rsu:
    post:
      tags: [Equity compensation]
      operationId: equityCompRsu
      summary: RSU vest → ordinary income, withholding gap, sell-to-cover
      description: >
        Breaks down a restricted-stock-unit vest: ordinary income (fmv × shares), the
        flat supplemental withholding (22% to $1M, 37% above), the taxpayer's true
        marginal tax on the income (computed via the tax engine, so it captures bracket
        creep, NIIT, and the Additional Medicare Tax), the resulting under/over
        withholding gap, the sell-to-cover share count, and the per-share cost basis.
        Deterministic; not tax advice.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/EquityCompRSURequest"
            examples:
              rsu:
                value:
                  tax_year: 2025
                  filing_status: single
                  other_ordinary_income: 150000
                  shares: 1000
                  fmv_at_vest: 50
      responses:
        "200":
          description: RSU vest breakdown
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EquityCompRSUResponse"
        "400":
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
        "405":
          description: Method not allowed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /v1/equity-comp/nso:
    post:
      tags: [Equity compensation]
      operationId: equityCompNso
      summary: NSO exercise → ordinary income, withholding gap, sell-to-cover
      description: >
        The spread on a non-qualified stock-option exercise ((fmv − strike) × shares)
        is ordinary compensation income at exercise (regular tax, not AMT). Returns the
        income, the flat supplemental withholding (22% to $1M, 37% above), the true
        marginal tax (via the tax engine), the under/over-withholding gap, the
        sell-to-cover share count, and the per-share basis (= fmv at exercise).
        Deterministic; not tax advice.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/EquityCompNSORequest"
            examples:
              nso:
                value:
                  tax_year: 2025
                  filing_status: single
                  other_ordinary_income: 150000
                  shares: 1000
                  strike_price: 5
                  fmv_at_exercise: 25
      responses:
        "200":
          description: NSO exercise breakdown
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EquityCompNSOResponse"
        "400":
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
        "405":
          description: Method not allowed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /v1/equity-comp/espp:
    post:
      tags: [Equity compensation]
      operationId: equityCompEspp
      summary: ESPP sale → qualifying vs disqualifying income split (§423)
      description: >
        Splits the gain on a §423 employee-stock-purchase-plan lot into ordinary income
        and capital gain under both a qualifying and a disqualifying disposition. The
        purchase price reflects the lookback discount (the discount off the lower of the
        offering- and purchase-date prices). Qualifying ordinary income is the lesser of
        the actual gain and the discount on the offering-date price; disqualifying
        ordinary income is the bargain at purchase. The tax of the split is the caller's
        (run /v1/tax/estimate with the components); the disqualifying capital gain's
        short/long character depends on the holding period from purchase. Not tax advice.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/EquityCompESPPRequest"
            examples:
              espp:
                value:
                  shares: 100
                  offering_date_price: 10
                  purchase_date_price: 20
                  sale_price: 30
                  discount_pct: 0.15
      responses:
        "200":
          description: ESPP income split
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EquityCompESPPResponse"
        "400":
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
        "405":
          description: Method not allowed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /v1/equity-comp/iso-amt-capacity:
    post:
      tags: [Equity compensation]
      operationId: equityCompIsoAmtCapacity
      summary: ISO AMT capacity — largest bargain element with no AMT
      description: >
        Finds the AMT crossover point: the largest ISO bargain element that can be
        exercised and held before the Alternative Minimum Tax begins, for the given
        income and filing status. AMT is monotone in the bargain element, so the answer
        is found by bisection through the tax §55 engine (bounded, ~60 iterations). If
        strike_price and fmv_at_exercise are given, the capacity is also expressed as a
        share count. Deterministic; not tax advice.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/EquityCompISOAMTCapacityRequest"
            examples:
              capacity:
                value:
                  tax_year: 2025
                  filing_status: single
                  ordinary_income: 100000
                  strike_price: 5
                  fmv_at_exercise: 25
      responses:
        "200":
          description: ISO AMT capacity
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EquityCompISOAMTCapacityResponse"
        "400":
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
        "405":
          description: Method not allowed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /v1/equity-comp/section-83b:
    post:
      tags: [Equity compensation]
      operationId: equityCompSection83b
      summary: §83(b) election — recognize at grant vs at vest
      description: >
        Compares electing under §83(b) (recognize the spread as ordinary income now, at
        grant / early exercise) against the default (recognize it at vest), for
        restricted stock or early-exercised options. Returns the ordinary income, the
        tax on it (current-year brackets, via the tax engine), and the per-share basis
        under each, plus the appreciation the election shifts from ordinary income into
        future capital-gain treatment. The time-value of paying now vs at vest and the
        eventual capital-gains rate are out of scope. Not tax advice.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/EquityCompSection83bRequest"
            examples:
              early_exercise:
                value:
                  tax_year: 2025
                  filing_status: single
                  other_ordinary_income: 150000
                  shares: 1000
                  price_paid: 1
                  fmv_at_grant: 1
                  fmv_at_vest: 20
      responses:
        "200":
          description: §83(b) comparison
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EquityCompSection83bResponse"
        "400":
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
        "405":
          description: Method not allowed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /v1/equity-comp/iso-schedule:
    post:
      tags: [Equity compensation]
      operationId: equityCompIsoSchedule
      summary: Multi-year ISO exercise plan that minimizes lifetime AMT
      description: >
        Plans a year-by-year ISO exercise schedule: each year it exercises as many
        shares as the AMT budget allows (default $0 = stay at the AMT crossover),
        accrues the AMT incurred as a §53 Minimum Tax Credit, and recovers that credit
        in later years when the regular tax exceeds the tentative minimum tax. Income
        and the share FMV vary by year (optional growth); the tax brackets are frozen
        at tax_year for every projected year (future brackets are not modeled). Bounded
        at horizon_years ≤ 40. Deterministic; not tax advice.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/EquityCompISOScheduleRequest"
            examples:
              spread_over_years:
                value:
                  tax_year: 2025
                  filing_status: single
                  total_shares: 6000
                  strike_price: 5
                  fmv_at_start: 25
                  annual_ordinary_income: 100000
                  horizon_years: 6
                  annual_amt_budget: 5000
      responses:
        "200":
          description: ISO exercise schedule
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EquityCompISOScheduleResponse"
        "400":
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
        "405":
          description: Method not allowed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /v1/tax/gain-harvesting:
    post:
      tags: [Tax]
      operationId: taxGainHarvesting
      summary: Tax-gain harvesting room + the tax of realizing a long-term gain
      description: >
        How much long-term capital gain can be realized at 0% then 15% (gains stack on
        ordinary taxable income), and — given a realize_amount — the tax of realizing it:
        the split across the 0%/15%/20% bands, the capital-gains tax, the incremental
        3.8% NIIT once MAGI crosses the §1411 threshold (supply magi), and the rate on
        the next gain dollar. Reuses the tax engine's LTCG breakpoints and NIIT
        threshold. Deterministic; not tax advice.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/GainHarvestingRequest"
            examples:
              harvest:
                value:
                  tax_year: 2025
                  filing_status: single
                  taxable_income: 40000
                  realize_amount: 20000
      responses:
        "200":
          description: Gain-harvesting room and realization tax
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/GainHarvestingResponse"
        "400":
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
        "405":
          description: Method not allowed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /v1/tax/tlh-benefit:
    post:
      tags: [Tax]
      operationId: taxTlhBenefit
      summary: Tax-loss-harvesting benefit (offset + deferral)
      description: >
        The benefit of harvesting a capital loss. The loss offsets capital gains first
        (at the LTCG rate), then up to $3,000 of ordinary income (at the marginal rate,
        §1211(b)); the rest carries forward. Because harvesting lowers the replacement's
        basis, the offset is recaptured as a future long-term gain — so the result nets
        the immediate saving against the present value of that recapture over the
        reinvestment horizon. Rates are derived from taxable income. Stateless (the
        caller supplies the loss); a wash-sale caveat is returned. Not tax advice.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/TLHBenefitRequest"
            examples:
              harvest:
                value:
                  tax_year: 2025
                  filing_status: single
                  taxable_income: 200000
                  harvestable_loss: 50000
                  offsettable_gains: 30000
                  reinvestment_years: 10
                  growth_rate: 0.07
      responses:
        "200":
          description: TLH benefit
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TLHBenefitResponse"
        "400":
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
        "405":
          description: Method not allowed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /v1/tax/asset-location:
    post:
      tags: [Tax]
      operationId: taxAssetLocation
      summary: Asset location — minimize annual tax drag by placement
      description: >
        For each asset, the annual tax drag per dollar of holding it in a taxable account
        (tax on its yield — qualified at the LTCG rate, the rest ordinary — plus the LTCG
        tax on the appreciation realized via turnover), and a recommended placement that
        fills the tax-advantaged accounts with the highest-drag assets first. Returns the
        annual drag under that placement vs a pro-rata baseline. A transparent calculator
        (the caller can override), not a black-box optimizer; Roth and deferred are
        combined as the shelter (the choice between them is a separate rate decision).
        Deterministic; not tax advice.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/AssetLocationRequest"
            examples:
              bonds_vs_equity:
                value:
                  tax_year: 2025
                  filing_status: single
                  taxable_income: 200000
                  taxable_balance: 100000
                  tax_advantaged_balance: 100000
                  assets:
                    - { name: bonds, amount: 100000, expected_return: 0.04, yield: 0.04, qualified_pct: 0, turnover: 0 }
                    - { name: equity, amount: 100000, expected_return: 0.08, yield: 0.02, qualified_pct: 1, turnover: 0.05 }
      responses:
        "200":
          description: Asset-location placement
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AssetLocationResponse"
        "400":
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
        "405":
          description: Method not allowed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /v1/tax/social-security-benefits:
    post:
      tags: [Tax]
      operationId: taxSocialSecurityBenefits
      summary: Taxation of Social Security benefits (§86) + the tax torpedo
      description: >
        The federally taxable portion of Social Security retirement benefits under IRC §86
        (the provisional / "combined income" worksheet, IRS Pub. 915): provisional income =
        other income (AGI excluding SS) + tax-exempt interest + ½ of benefits, taxed in two
        tiers (up to 50%, then up to 85%) above the statutory base amounts ($25k/$34k single,
        $32k/$44k joint). Surfaces the "tax torpedo" — the band where each extra dollar of
        other income also drags $0.50 or $0.85 of benefits into tax, so an optional
        marginal_ordinary_rate yields the true effective marginal rate (e.g. a 22% bracket
        becomes 40.7%). The §86 thresholds are statutory and not inflation-indexed, so this
        engine is not year-keyed. Deterministic; not tax advice.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/SSTaxRequest"
            examples:
              retiree_in_torpedo:
                summary: Single retiree, $24k benefits, in the 85% band
                value:
                  filing_status: single
                  social_security_benefits: 24000
                  other_income: 35000
                  tax_exempt_interest: 2000
                  marginal_ordinary_rate: 0.22
      responses:
        "200":
          description: Taxable Social Security + torpedo metrics
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SSTaxResponse"
        "400":
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
        "405":
          description: Method not allowed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /v1/tax/marginal-map:
    post:
      tags: [Tax]
      operationId: taxMarginalMap
      summary: Effective-marginal-rate map (tax topography)
      description: >
        Sweeps one income variable and plots the TRUE marginal rate on the next dollar across
        the range, surfacing the Social Security tax torpedo, IRMAA cliffs, the ACA 400%-of-FPL
        subsidy cliff, NIIT, and LTCG 0/15/20 stacking in one curve. A pure composite of the
        tax, sstax, irmaa, and healthcare engines: at each grid point it totals the annual cost
        (income tax + NIIT + Additional Medicare + optional IRMAA surcharge + optional ACA net
        premium) and forward-differences it. Each point's marginal rate is split into additive
        components (ordinary, ss_inclusion, ltcg, niit, irmaa_step, aca_step), and discrete
        cliffs (IRMAA tiers, the ACA cliff) are reported separately. Deterministic; not tax advice.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/MarginalMapRequest"
            examples:
              torpedo_and_irmaa:
                summary: Single retiree ($35k SS) sweeping IRA withdrawals, with IRMAA
                value:
                  tax_year: 2025
                  filing_status: single
                  ordinary_income: 20000
                  social_security_benefits: 35000
                  sweep_variable: ordinary
                  sweep_start: 0
                  sweep_stop: 100000
                  sweep_step: 1000
                  include_irmaa: true
                  medicare_beneficiaries: 1
      responses:
        "200":
          description: The marginal-rate curve + cliff locations
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MarginalMapResponse"
        "400":
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
        "405":
          description: Method not allowed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /v1/social-security/couple-claim:
    post:
      tags: [Social Security]
      operationId: socialSecurityCoupleClaim
      summary: Couple / household Social Security claiming optimizer
      description: >
        Finds the claiming ages for a married couple that maximize the household's expected
        present value, accounting for spousal benefits while both are alive and the survivor
        benefit the surviving spouse keeps for life. A composite of the SSA mortality table and
        the single-person reduction / delayed-retirement-credit factors (no new data). Returns
        the optimal (claim_age_a, claim_age_b), the full expected-PV surface for a heatmap, and
        the gain over both-at-FRA / both-at-70 — so the canonical result is explicit: the higher
        earner usually delays (the survivor benefit), the lower earner can claim earlier.
        Benefits are real (today's) dollars; discount_rate is real. Deterministic; not advice.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CoupleClaimRequest"
            examples:
              higher_lower_earner:
                summary: A higher earner + lower earner, both 62
                value:
                  spouse_a: { pia_monthly: 3000, birth_year: 1963, current_age: 62, sex: male }
                  spouse_b: { pia_monthly: 1200, birth_year: 1963, current_age: 62, sex: female }
                  discount_rate: 0.02
      responses:
        "200":
          description: The optimal claim ages + the expected-PV surface
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CoupleClaimResponse"
        "400":
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
        "405":
          description: Method not allowed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /v1/tax/qcd-plan:
    post:
      tags: [Tax]
      operationId: taxQcdPlan
      summary: Qualified charitable distribution (QCD) planner
      description: >
        For an IRA owner 70½+, a qualified charitable distribution (IRC §408(d)(8)) gives
        directly from the IRA to charity: it is excluded from gross income and counts toward the
        RMD, so it keeps the gift out of MAGI — lowering IRMAA and the §86 taxation of Social
        Security — and it helps even the standard-deduction retiree (for whom a charitable
        itemized deduction is worthless). Compares QCD vs take-the-RMD-then-donate by composing
        the rmd, sstax, irmaa, and tax engines, and reports each strategy's AGI / MAGI / taxable
        SS / income tax / IRMAA plus the net tax saving. The QCD annual limit is the IRS-indexed
        §408(d)(8) amount ($108,000 in 2025, $111,000 in 2026). Deterministic; not tax advice.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/QCDPlanRequest"
            examples:
              standard_deduction_retiree:
                summary: Single 75-year-old, $1.2M IRA, $30k gift, standard deduction
                value:
                  tax_year: 2025
                  filing_status: single
                  age: 75
                  ira_balance: 1200000
                  intended_gift: 30000
                  ordinary_income: 90000
                  social_security_benefits: 40000
                  medicare_beneficiaries: 1
      responses:
        "200":
          description: The QCD-vs-take-RMD comparison
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/QCDPlanResponse"
        "400":
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
        "405":
          description: Method not allowed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /v1/tax/estimated-tax:
    post:
      tags: [Tax]
      operationId: taxEstimatedTax
      summary: Estimated-tax / §6654 safe-harbor + underpayment-penalty estimate
      description: >
        Evaluates the §6654 estimated-tax safe harbors for an individual and estimates the
        underpayment penalty if the safe harbor is missed — for anyone with lumpy or
        under-withheld income (equity comp, business owners, Roth converters). The required
        annual payment is the lesser of 90% of the current year's tax or the prior-year safe
        harbor (100% of last year's tax, or 110% when prior-year AGI exceeds $150,000 / $75,000
        MFS). No penalty applies if the balance due after withholding is under $1,000, or the
        prior year had zero tax. The penalty is estimated with the regular method (four equal
        installments, withholding credited evenly, overpayments carried forward) at the IRS
        underpayment rate — 7% for 2025 and 2026 by default, overridable per request since the
        penalty accrues into a year whose quarterly rates are unpublished. Deterministic; not tax advice.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/EstimatedTaxRequest"
            examples:
              high_income_underwithheld:
                summary: Single filer, high prior-year AGI (110% harbor), under-withheld
                value:
                  tax_year: 2026
                  filing_status: single
                  prior_year_tax: 20000
                  prior_year_agi: 200000
                  projected_current_tax: 30000
                  withholding: 8000
                  estimated_payments: [2000, 2000, 0, 0]
      responses:
        "200":
          description: The safe-harbor evaluation and penalty estimate
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EstimatedTaxResponse"
        "400":
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
        "405":
          description: Method not allowed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /v1/tax/survivor-transition:
    post:
      tags: [Tax]
      operationId: taxSurvivorTransition
      summary: Survivor / widow(er)'s-penalty tax transition projection
      description: >
        Projects the "widow's penalty": when the first spouse dies, the survivor files single
        instead of jointly — brackets compress, the §86 Social Security and IRMAA thresholds
        roughly halve, and one Social Security benefit stops. Prices the household both ways
        (MFJ while both alive vs the survivor filing single) by composing the tax, sstax, and
        irmaa engines, and reports each scenario's AGI / MAGI / taxable SS / income tax / IRMAA,
        the annual widow's penalty, the income lost, and the marginal-rate jump. With
        conversion_amount, it also values accelerating a Roth conversion into the cheaper
        still-MFJ years. Deterministic; not tax advice.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/SurvivorTransitionRequest"
            examples:
              retired_couple:
                summary: Retired couple, $90k ordinary + two SS benefits, $50k conversion
                value:
                  tax_year: 2026
                  ordinary_income: 90000
                  qualified_income: 15000
                  social_security_higher: 36000
                  social_security_lower: 20000
                  conversion_amount: 50000
      responses:
        "200":
          description: The MFJ-vs-survivor comparison and conversion arbitrage
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SurvivorTransitionResponse"
        "400":
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
        "405":
          description: Method not allowed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /v1/retirement/sepp:
    post:
      tags: [Retirement]
      operationId: retirementSepp
      summary: 72(t) / SEPP early-withdrawal schedule
      description: >
        Substantially Equal Periodic Payments under §72(t) let someone draw an IRA before age
        59½ without the 10% early-withdrawal penalty. Computes the annual payment under the
        RMD and fixed-amortization methods (both on the IRS Single Life Table), the recommended
        (largest) figure, the required duration (the longer of five years or until age 59½), and
        the worst-case retroactive penalty if the plan is busted. The amortization rate defaults
        to the 5% statutory floor (always allowable); pass a higher rate up to the greater of 5%
        or 120% of the mid-term AFR to lift the payment. The fixed-annuitization method is not
        modeled. Deterministic; not tax advice.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/SeppRequest"
            examples:
              early_retiree:
                summary: Age 50, $500k IRA, 5% floor rate
                value:
                  account_balance: 500000
                  age: 50
                  interest_rate: 0.05
      responses:
        "200":
          description: The SEPP schedule
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SeppResponse"
        "400":
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
        "405":
          description: Method not allowed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /v1/insurance/life-needs:
    post:
      tags: [Insurance]
      operationId: insuranceLifeNeeds
      summary: Life-insurance needs analysis (capital-needs / DIME + human-life-value)
      description: >
        Sizes how much life insurance a household needs and the gap against coverage already in
        force. The capital-needs (DIME) method sums final expenses, debts, the mortgage, an
        education fund, an emergency fund, and the present value of the survivors' income gap over
        a chosen horizon (default 70% of income, discounted at a real rate), then subtracts existing
        assets and insurance for the recommended additional coverage. When years_to_retirement is
        given, it also reports the human-life-value cross-check (present value of future earnings net
        of self-consumption). Deterministic; not insurance advice.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/LifeNeedsRequest"
            examples:
              family_breadwinner:
                summary: $100k earner, mortgage + kids, some existing coverage
                value:
                  annual_income: 100000
                  income_replace_years: 20
                  income_replace_fraction: 0.7
                  survivor_annual_income: 40000
                  final_expenses: 15000
                  debts: 20000
                  mortgage_balance: 250000
                  education_fund: 100000
                  existing_assets: 50000
                  existing_life_insurance: 200000
                  years_to_retirement: 30
                  effective_tax_rate: 0.2
      responses:
        "200":
          description: The needs analysis
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LifeNeedsResponse"
        "400":
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
        "405":
          description: Method not allowed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /v1/insurance/disability-needs:
    post:
      tags: [Insurance]
      operationId: insuranceDisabilityNeeds
      summary: Disability-income protection gap analysis
      description: >
        Sizes the gap between the income a household needs if the earner is disabled and what
        existing coverage pays after tax. Its main value is netting benefits correctly: an
        employer-paid group LTD benefit is taxable, so "60% of pay" nets far less at a given
        marginal rate, while an individually-owned (after-tax) policy pays tax-free. Reports the
        monthly target (a replacement fraction of income, or an essential-expense floor), the net
        value of group / individual / SSDI benefits, the remaining monthly gap and the additional
        benefit to buy, plus the current replacement ratio. Deterministic; not insurance advice.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/DisabilityNeedsRequest"
            examples:
              taxable_group_only:
                summary: $120k earner with employer-paid 60% group LTD (taxable) + a small individual policy
                value:
                  gross_annual_income: 120000
                  target_replacement_fraction: 0.65
                  employer_group_fraction: 0.6
                  employer_paid_premium: true
                  marginal_tax_rate: 0.3
                  individual_monthly_benefit: 1500
                  ssdi_monthly_benefit: 0
      responses:
        "200":
          description: The protection-gap analysis
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DisabilityNeedsResponse"
        "400":
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
        "405":
          description: Method not allowed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /v1/insurance/long-term-care:
    post:
      tags: [Insurance]
      operationId: insuranceLongTermCare
      summary: Long-term-care cost projection + self-insure-vs-insure
      description: >
        Projects the cost of a long-term-care episode — today's annual cost (a required input, since
        it varies widely by region and setting) inflated to the expected claim age, run over the care
        duration, and present-valued — and reports the lump sum needed to self-insure. When a policy
        is described (premium + benefit), it nets premiums and out-of-pocket against the covered
        benefit, returning the net cost with the policy, the coverage ratio, and how much insuring
        saves on this central scenario. Note this is an expected-cost comparison; the real value of
        LTC insurance is tail protection (a long claim), so model longer durations to see it.
        Deterministic; not insurance advice.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/LongTermCareRequest"
            examples:
              age60_with_policy:
                summary: Age 60, care at 82, $100k/yr today for 3 years, vs a $3k/yr policy
                value:
                  current_age: 60
                  care_start_age: 82
                  annual_care_cost_today: 100000
                  care_duration_years: 3
                  care_cost_inflation: 0.045
                  discount_rate: 0.04
                  annual_premium: 3000
                  policy_annual_benefit: 90000
                  policy_benefit_inflation: 0.03
      responses:
        "200":
          description: The LTC projection and (optional) policy comparison
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LongTermCareResponse"
        "400":
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
        "405":
          description: Method not allowed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /v1/portfolio/rebalance:
    post:
      tags: [Portfolio]
      operationId: portfolioRebalance
      summary: Portfolio rebalancing (drift, trades, tax-aware contributions-first)
      description: >
        Given holdings and target weights, computes the trades that restore the targets, the
        per-holding drift, and whether each holding is outside a tolerance band. Contributions and
        withdrawals scale the investable base first. When tax_aware is set, it estimates the realized
        gain of a full rebalance and — if there's new cash and no withdrawal — a contributions-first
        plan that directs the cash to underweight holdings without selling (the tax-efficient way to
        rebalance a taxable account). Target weights must sum to 1. Deterministic; not investment advice.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/RebalanceRequest"
            examples:
              four_fund_with_cash:
                summary: A four-fund portfolio with $10k new cash, tax-aware
                value:
                  holdings:
                    - {label: us_stocks, value: 45000, target_weight: 0.40, cost_basis: 30000}
                    - {label: intl_stocks, value: 20000, target_weight: 0.20, cost_basis: 15000}
                    - {label: bonds, value: 25000, target_weight: 0.30, cost_basis: 24000}
                    - {label: reits, value: 10000, target_weight: 0.10, cost_basis: 8000}
                  new_contribution: 10000
                  band: 0.05
                  tax_aware: true
      responses:
        "200":
          description: The rebalancing plan
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RebalanceResponse"
        "400":
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
        "405":
          description: Method not allowed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /v1/portfolio/glidepath:
    post:
      tags: [Portfolio]
      operationId: portfolioGlidepath
      summary: Age-based equity/bond glidepath
      description: >
        Generates an age-based equity/bond allocation path — the target the rebalance endpoint aims at
        as the investor ages. The rule_of_thumb method sets equity = clamp((rule_base − age)/100, floor,
        ceiling) (the "own your age in bonds" family). The linear (target-date) method glides equity from
        start_equity at current_age to end_equity at retirement_age, then holds it ("to") or continues to
        final_equity at final_age ("through"). Returns a year-by-year path plus the current, retirement,
        and end allocations. Deterministic; not investment advice.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/GlidepathRequest"
            examples:
              target_date_through:
                summary: Age 40, retire at 65, 80%→40% equity gliding through to 30% by 80
                value:
                  current_age: 40
                  end_age: 90
                  method: linear
                  retirement_age: 65
                  start_equity: 0.8
                  end_equity: 0.4
                  glide_type: through
                  final_equity: 0.3
                  final_age: 80
      responses:
        "200":
          description: The generated glidepath
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/GlidepathResponse"
        "400":
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
        "405":
          description: Method not allowed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /v1/education/student-loan:
    post:
      tags: [Education]
      operationId: educationStudentLoan
      summary: Federal student-loan repayment comparison (standard + RAP + IBR + PSLF)
      description: >
        Compares federal repayment options under current law (P.L. 119-21 / OBBBA): the standard
        10-year amortization, the new Repayment Assistance Plan (RAP, available July 1 2026), and
        Income-Based Repayment (IBR), with an optional PSLF outcome. RAP is a flat % of AGI by
        bracket (1%–10%, less $50/mo per dependent, $10 floor) that waives unpaid interest and
        matches principal up to $50/mo; IBR is 10%/15% of discretionary income (AGI − 150% of the
        HHS poverty guideline) capped at the standard payment, forgiven at 20/25 years. Projections
        hold income constant. Deterministic; not financial advice (verify plan rules with your servicer).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/StudentLoanRequest"
            examples:
              public_service_borrower:
                summary: $60k balance at 6.5%, $55k AGI, family of 2, pursuing PSLF
                value:
                  loan_balance: 60000
                  interest_rate: 0.065
                  agi: 55000
                  family_size: 2
                  dependents: 1
                  pursuing_pslf: true
      responses:
        "200":
          description: The repayment comparison
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/StudentLoanResponse"
        "400":
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
        "405":
          description: Method not allowed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /v1/education/student-loan-refi:
    post:
      tags: [Education]
      operationId: educationStudentLoanRefi
      summary: Refinance federal loans privately, or stay federal? (forgiveness-aware)
      description: >
        Should you refinance federal student loans with a private lender? A private refi usually
        offers a lower fixed rate but permanently forfeits federal protections — income-driven
        repayment, forgiveness, and PSLF. This runs the borrower's best federal option (reusing the
        student-loan comparison) and weighs its present value of payments against a private refi's,
        so the rate saving and the forgiveness given up sit on the same scale. It also reports the
        refi rate that would merely tie the plain standard federal plan (the pure-rate breakeven),
        and which federal balance would be forfeited. Deterministic; not lending, tax, or financial
        advice.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/StudentLoanRefiRequest"
            examples:
              high_earner:
                summary: $80k at 7.5%, $200k AGI (no forgiveness) vs a 5% 10-yr private refi
                value:
                  federal:
                    loan_balance: 80000
                    interest_rate: 0.075
                    agi: 200000
                    family_size: 1
                  refi_rate: 0.05
                  refi_term_months: 120
                  discount_rate: 0.05
      responses:
        "200":
          description: The refi-vs-federal comparison
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/StudentLoanRefiResponse"
        "400":
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
        "405":
          description: Method not allowed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /v1/education/aid-estimate:
    post:
      tags: [Education]
      operationId: educationAidEstimate
      summary: FAFSA Student Aid Index (SAI) + financial need
      description: >
        Estimates the FAFSA Student Aid Index (the figure that replaced the EFC for 2024-25+) under
        the Federal Need Analysis Methodology, Formula A (dependent students), and the resulting
        financial need. SAI = parents' contribution (income less allowances, plus 12% of net worth,
        run through the progressive AAI schedule) + the student's contribution from income (50%) and
        assets (20%). Uses the 2026-27 ED tables (income-protection allowance, business/farm
        adjustment, AAI schedule). With cost_of_attendance it returns need = COA − SAI. Deterministic;
        an estimate, not an official determination.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/AidEstimateRequest"
            examples:
              dependent_family_of_four:
                summary: MFJ family of 4, $90k AGI, two earners, some savings
                value:
                  award_year: 2026
                  family_size: 4
                  parent_filing_status: mfj
                  parent_agi: 90000
                  parent_income_tax_paid: 8000
                  parent_earned_income: 60000
                  parent2_earned_income: 30000
                  parent_cash_savings: 20000
                  parent_investments_net: 50000
                  student_agi: 5000
                  student_earned_income: 5000
                  cost_of_attendance: 30000
      responses:
        "200":
          description: The SAI and financial-need estimate
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AidEstimateResponse"
        "400":
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
        "405":
          description: Method not allowed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /v1/education/529-to-roth:
    post:
      tags: [Education]
      operationId: education529ToRoth
      summary: 529→Roth IRA rollover eligibility (SECURE 2.0 §126)
      description: >
        How much of a 529 balance can be rolled into the beneficiary's Roth IRA under SECURE 2.0
        §126? The rollover is gated: the account must be 15+ years old, contributions from the last
        5 years are excluded, each year is capped at the Roth IRA contribution limit (less the
        beneficiary's other IRA contributions for the year) and by the beneficiary's earned income,
        and there is a $35,000 lifetime cap per beneficiary. Returns the maximum this year, which
        cap binds, and how many years to exhaust the lifetime limit. Deterministic; not tax advice.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/Rollover529Request"
            examples:
              graduate_with_leftover:
                summary: $40k balance, 18-yr-old account, $5k contributed in the last 5 years
                value:
                  year: 2026
                  beneficiary_age: 25
                  balance: 40000
                  account_age_years: 18
                  recent_contributions: 5000
                  beneficiary_earned_income: 50000
      responses:
        "200":
          description: The rollover analysis
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Rollover529Response"
        "400":
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
        "405":
          description: Method not allowed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /v1/retirement/self-employed:
    post:
      tags: [Retirement]
      operationId: retirementSelfEmployed
      summary: Solo 401(k) vs SEP-IRA contribution sizing (self-employed)
      description: >
        Sizes the maximum retirement contribution for a self-employed person (sole proprietor /
        single-member LLC) under both a SEP-IRA and a solo 401(k) from Schedule C net profit. It
        applies the self-employment-tax adjustment (net profit − ½ SE tax = plan compensation) and
        the self-employed "reduced rate" (a 25% employer plan is 20% of plan compensation), adds the
        solo 401(k) employee elective deferral plus the age catch-up, and caps everything at the
        §415(c) annual-additions limit. Returns both plan totals and which lets you save more.
        Deterministic; not tax advice.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/SelfEmployedRequest"
            examples:
              sole_proprietor:
                summary: $100k Schedule C net profit, age 40
                value:
                  tax_year: 2026
                  age: 40
                  net_profit: 100000
      responses:
        "200":
          description: The contribution sizing
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SelfEmployedResponse"
        "400":
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
        "405":
          description: Method not allowed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /v1/tax/w4-checkup:
    post:
      tags: [Tax]
      operationId: taxW4Checkup
      summary: Paycheck-withholding (W-4) checkup
      description: >
        Are you on track with federal withholding? Projects the year's federal tax liability
        (reusing the tax engine) from the embedded tax situation, compares it against what will be
        withheld at the current pace (year-to-date plus the per-paycheck amount over the remaining
        pay periods), and reports the expected refund or balance due, the status, and the
        per-paycheck W-4 adjustment (line 4c) to land on target. Supply the prior year's tax to also
        check the §6654 safe harbor and flag underpayment-penalty risk. Deterministic; not tax advice.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/W4CheckupRequest"
            examples:
              single_earner:
                summary: $90k single, $6k withheld YTD, $500/check, 12 checks left
                value:
                  tax:
                    tax_year: 2026
                    filing_status: single
                    ordinary_income: 90000
                    wages: 90000
                  ytd_withheld: 6000
                  per_paycheck_withholding: 500
                  pay_periods_remaining: 12
                  prior_year_tax: 13000
                  prior_year_agi: 88000
      responses:
        "200":
          description: The withholding checkup
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/W4CheckupResponse"
        "400":
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
        "405":
          description: Method not allowed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /v1/batch:
    post:
      tags: [Platform]
      operationId: batch
      summary: Batch — run up to 20 calc sub-requests in one call
      description: >
        Runs up to 20 calculation sub-requests in a single call, in order, returning one result
        per sub-request. Each sub-request is dispatched through the same authenticated, metered
        handler as a direct REST call (so it records one usage event and is validated identically),
        and a sub-failure (bad path, bad body) is isolated — it does not fail the batch, which
        always returns 200. Sub-request paths must be calc routes (no /v1/batch nesting, no
        /v1/usage or /v1/signup). Rate limiting is all-or-nothing: the batch reserves one token per
        sub-request up front and is rejected whole with 429 if the budget can't cover them. The
        whole batch shares one calc deadline budget. Deterministic — identical batches produce an
        identical result_id.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/BatchRequest"
            examples:
              tvm_and_rmd:
                summary: Two small sub-requests (a mortgage payment + an RMD)
                value:
                  requests:
                    - path: /v1/tvm
                      body: { solve_for: pmt, rate: 0.0033333333, nper: 360, pv: -200000, fv: 0 }
                    - path: /v1/retirement/rmd
                      body: { age: 73, account_balance: 1000000, birth_year: 1953 }
      responses:
        "200":
          description: One result per sub-request, in order (sub-failures are isolated, not fatal)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/BatchResponse"
        "400":
          description: Invalid request (malformed JSON, or not 1–20 sub-requests)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
        "405":
          description: Method not allowed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /v1/savings-waterfall:
    post:
      tags: [Retirement]
      operationId: savingsWaterfall
      summary: Savings waterfall — next-dollar ordering of annual savings
      description: >
        Answers "I have $X a year to save — where does each dollar go?" by filling the standard
        tax-advantaged waterfall in priority order: 401(k) to the full employer match, then HSA,
        then IRA (annotated with the Roth route — direct / backdoor / traditional), then the rest
        of the 401(k) elective-deferral limit, then a taxable account. Reports the dollar to each
        bucket, the employer match captured, and the unused tax-advantaged room. Composes the
        contribution-limit and IRA-eligibility engines, so the limits stay current automatically.
        Deterministic; not financial advice.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/SavingsWaterfallRequest"
            examples:
              single_earner_with_match:
                summary: Single, $120k salary, $30k to save, 50%-up-to-6% match, family HDHP
                value:
                  tax_year: 2026
                  age: 40
                  filing_status: single
                  magi: 90000
                  compensation: 120000
                  annual_savings: 30000
                  hsa_coverage: family
                  employer_match_rate: 0.5
                  employer_match_cap_pct: 0.06
      responses:
        "200":
          description: The ordered allocation plan
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SavingsWaterfallResponse"
        "400":
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
        "405":
          description: Method not allowed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /v1/debt-payoff:
    post:
      tags: [Utilities]
      operationId: debtPayoff
      summary: Multi-debt payoff plan (avalanche vs snowball)
      description: >
        Given several debts and a fixed monthly budget, simulates the avalanche (highest interest
        rate first) and snowball (smallest balance first) strategies month by month — paying the
        minimum on every debt and rolling the extra (plus freed-up minimums) into the target — and
        returns the payoff order, months, and total interest for each, plus how much interest and
        time avalanche saves. If the budget can't overcome the interest, the debt is flagged as not
        paid off within a 50-year horizon. Deterministic; not financial advice.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/DebtPayoffRequest"
            examples:
              three_debts:
                summary: Two cards + a car loan, $900/mo, comparing both strategies
                value:
                  debts:
                    - {label: credit_card, balance: 8000, annual_rate: 0.229, minimum_payment: 160}
                    - {label: store_card, balance: 3500, annual_rate: 0.189, minimum_payment: 70}
                    - {label: car_loan, balance: 14000, annual_rate: 0.06, minimum_payment: 320}
                  monthly_budget: 900
                  strategy: both
      responses:
        "200":
          description: The payoff plan(s)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DebtPayoffResponse"
        "400":
          description: Invalid request (e.g. budget below the sum of minimum payments)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
        "405":
          description: Method not allowed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /v1/roth-vs-traditional:
    post:
      tags: [Retirement]
      operationId: rothVsTraditional
      summary: Roth vs traditional contribution comparator
      description: >
        Compares a Roth vs a traditional (pre-tax) retirement contribution on an apples-to-apples,
        after-tax basis. The same annual dollars go into either account; the traditional path also
        invests its up-front tax saving in a taxable side account (matching the out-of-pocket cost),
        whose gains are taxed at the end. Roth withdrawals are tax-free; traditional withdrawals are
        taxed at the retirement marginal rate. Returns each path's after-tax future value, the Roth
        advantage, the recommendation, and the breakeven retirement rate. Deterministic; not advice.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/RothVsTraditionalRequest"
            examples:
              slightly_lower_in_retirement:
                summary: $7k/yr for 30 yr at 7%, 24% now vs 22% in retirement
                value:
                  annual_contribution: 7000
                  years: 30
                  expected_return: 0.07
                  current_marginal_rate: 0.24
                  retirement_marginal_rate: 0.22
      responses:
        "200":
          description: The after-tax comparison
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RothVsTraditionalResponse"
        "400":
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
        "405":
          description: Method not allowed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /v1/tax/filing-status:
    post:
      tags: [Tax]
      operationId: taxFilingStatus
      summary: Filing-status comparator — MFJ vs MFS + marriage penalty/bonus
      description: >
        For a married couple, compares total federal tax filing jointly (one combined return) vs
        married-filing-separately (two returns), and reports the marriage penalty or bonus (the
        joint tax vs what the two would owe as single filers). Runs the tax engine five times. On
        base tax MFJ is essentially never worse than MFS; the value is quantifying the gap and the
        marriage effect. MFS credit disallowances (EITC, education, dependent-care) are not modeled.
        Deterministic; not tax advice.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/FilingStatusRequest"
            examples:
              two_earners:
                summary: $180k + $60k earners
                value:
                  tax_year: 2026
                  spouse_a: { ordinary_income: 180000, wages: 180000 }
                  spouse_b: { ordinary_income: 60000, wages: 60000 }
      responses:
        "200":
          description: The MFJ-vs-MFS comparison and marriage effect
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FilingStatusResponse"
        "400":
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
        "405":
          description: Method not allowed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /v1/benefits/health-plan:
    post:
      tags: [Healthcare]
      operationId: benefitsHealthPlan
      summary: Health-plan comparison — HDHP+HSA vs PPO total cost
      description: >
        Compares the total annual cost of a high-deductible health plan with an HSA against a
        traditional PPO, for an expected level of medical spending. Each plan's cost is its premium
        plus the member's cost-sharing (spend up to the deductible, then coinsurance, capped at the
        out-of-pocket max); the HDHP nets out the employer HSA seed and the tax saving on the HSA
        contribution, and the PPO nets any health-FSA tax saving. Returns each plan's cost breakdown,
        the recommendation, and the difference. Deterministic; not insurance advice.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/HealthPlanRequest"
            examples:
              hdhp_vs_ppo:
                summary: $6k expected spend, 30% marginal rate, employer $1k HSA seed
                value:
                  expected_medical_spend: 6000
                  marginal_tax_rate: 0.30
                  hdhp: { premium: 1200, deductible: 3000, oop_max: 6000, coinsurance: 0.20 }
                  ppo: { premium: 3600, deductible: 500, oop_max: 4000, coinsurance: 0.20 }
                  employer_hsa_seed: 1000
                  hsa_contribution: 3000
      responses:
        "200":
          description: The plan-cost comparison
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/HealthPlanResponse"
        "400":
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
        "405":
          description: Method not allowed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /v1/benefits/dependent-care:
    post:
      tags: [Tax]
      operationId: benefitsDependentCare
      summary: Dependent-care FSA vs §21 credit comparator
      description: >
        Compares the two ways to get a tax benefit on child- and dependent-care costs: a
        dependent-care FSA (§129 — pre-tax, saving income + FICA at the marginal rate) vs the §21
        child- and dependent-care tax credit (a percentage of eligible expenses). The §21 credit is
        computed by the tax engine, so its applicable-percentage schedule (incl. the 2026 OBBBA
        changes), the $3k/$6k expense cap by qualifying count, the AGI phase-down, and the
        nonrefundable limit all apply. Returns both benefits and the recommendation. Deterministic;
        not tax advice.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/DependentCareRequest"
            examples:
              two_kids:
                summary: MFJ $120k, $6k of care for 2 kids, 30% marginal rate
                value:
                  tax_year: 2025
                  filing_status: mfj
                  ordinary_income: 120000
                  dependent_care_expenses: 6000
                  qualifying_count: 2
                  marginal_tax_rate: 0.30
      responses:
        "200":
          description: The FSA-vs-credit comparison
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DependentCareResponse"
        "400":
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
        "405":
          description: Method not allowed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /v1/mortgage/affordability:
    post:
      tags: [Utilities]
      operationId: mortgageAffordability
      summary: Home affordability — max price/loan from income and DTI
      description: >
        Estimates the maximum home price and loan a buyer can afford from income, existing monthly
        debts, and a down payment, under the standard front-end (housing / income, default 28%) and
        back-end (total debt / income, default 36%) DTI limits. Solves for the price given that
        property tax, insurance, and PMI scale with the home value (a fixed-point iteration), and
        returns the binding limit plus the monthly PITI + HOA + PMI breakdown at the max.
        Deterministic; not lending advice.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/AffordabilityRequest"
            examples:
              standard:
                summary: $120k income, $300/mo other debt, $60k down at 6%
                value:
                  annual_income: 120000
                  monthly_debts: 300
                  down_payment: 60000
                  interest_rate: 0.06
      responses:
        "200":
          description: The affordability estimate
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AffordabilityResponse"
        "400":
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
        "405":
          description: Method not allowed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /v1/mortgage/refinance:
    post:
      tags: [Utilities]
      operationId: mortgageRefinance
      summary: Refinance breakeven — current vs new mortgage
      description: >
        Compares the current mortgage payment against a refinance and reports the monthly saving,
        the months to recoup the closing costs (the breakeven), and the total-cost comparison over
        each loan's remaining life. Closing costs can be rolled into the new loan. Note a longer
        new term lowers the payment but can raise total interest. Deterministic; not lending advice.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/RefinanceRequest"
            examples:
              rate_drop:
                summary: $300k at 7% with 25 yr left → a 30-yr at 5.5%, $6k costs
                value:
                  current_balance: 300000
                  current_rate: 0.07
                  current_remaining_months: 300
                  new_rate: 0.055
                  new_term_months: 360
                  closing_costs: 6000
      responses:
        "200":
          description: The refinance analysis
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RefinanceResponse"
        "400":
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
        "405":
          description: Method not allowed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /v1/mortgage/points:
    post:
      tags: [Utilities]
      operationId: mortgagePoints
      summary: Discount-points buydown breakeven
      description: >
        Should you pay discount points to buy down the mortgage rate? Compares the base-rate
        payment to the bought-down payment and reports the points cost (each point = 1% of the
        loan), the monthly saving, the months to recoup that cost (the breakeven), and the net
        saving if the loan is held to term. Deterministic; not lending advice.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/PointsRequest"
            examples:
              two_points:
                summary: $400k 30-yr, 2 points buy 7% down to 6.75%
                value:
                  loan_amount: 400000
                  term_months: 360
                  base_rate: 0.07
                  buydown_rate: 0.0675
                  points: 2
      responses:
        "200":
          description: The points analysis
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PointsResponse"
        "400":
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
        "405":
          description: Method not allowed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /v1/mortgage/term-comparison:
    post:
      tags: [Utilities]
      operationId: mortgageTermComparison
      summary: 15-vs-30 term comparison with invest-the-difference
      description: >
        Pay the mortgage down fast, or take the longer term and invest the difference? Holds the
        monthly budget equal to the higher (short-term) payment: the short-term path pays the house
        off sooner and then invests the freed-up payment; the long-term path invests the monthly
        difference throughout. Reports the interest each path pays and the terminal wealth at the
        longer horizon, and names the winner. Deterministic; not lending or investment advice.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/TermComparisonRequest"
            examples:
              fifteen_vs_thirty:
                summary: $400k — 15-yr at 6.25% vs 30-yr at 7%, 5% investment return
                value:
                  loan_amount: 400000
                  short_term_months: 180
                  short_rate: 0.0625
                  long_term_months: 360
                  long_rate: 0.07
                  investment_return: 0.05
      responses:
        "200":
          description: The term comparison
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TermComparisonResponse"
        "400":
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
        "405":
          description: Method not allowed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /v1/mortgage/rent-vs-buy:
    post:
      tags: [Utilities]
      operationId: mortgageRentVsBuy
      summary: Rent vs buy over a holding period (present value)
      description: >
        Compares the cost of buying a home against renting over a fixed holding period on a
        present-value basis. Every cash flow — mortgage, property tax, insurance, maintenance, HOA,
        rent, and the net proceeds from selling at the end — is discounted to today at the
        investment return, so the opportunity cost of the down payment is captured by the discount
        rate. The path with the lower present value of net cost wins. Most fields default to
        reasonable assumptions; deterministic; not lending, tax, or financial advice.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/RentVsBuyRequest"
            examples:
              seven_year_hold:
                summary: $500k home, 20% down, 7% mortgage vs $2,500/mo rent, 7-yr hold
                value:
                  home_price: 500000
                  down_payment: 100000
                  mortgage_rate: 0.07
                  loan_term_months: 360
                  monthly_rent: 2500
                  holding_period_months: 84
                  investment_return: 0.05
                  home_appreciation: 0.03
                  rent_growth: 0.03
      responses:
        "200":
          description: The rent-vs-buy comparison
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RentVsBuyResponse"
        "400":
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
        "405":
          description: Method not allowed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /v1/healthcare/aca-optimize:
    post:
      tags: [Healthcare]
      operationId: healthcareAcaOptimize
      summary: ACA premium-tax-credit withdrawal optimizer (pre-65 early retirees)
      description: >
        For a pre-65 early retiree, finds the account-withdrawal mix that funds a spending need
        while minimizing MAGI — maximizing the ACA premium tax credit and staying under the
        400%-of-FPL subsidy cliff (which returns in 2026). Each source adds different MAGI per
        dollar (Roth 0, taxable only the gain fraction, tax-deferred full), so the MAGI-optimal
        order is Roth → taxable → tax-deferred. Compares that plan to a naive proportional draw
        and reports each plan's draw mix / MAGI / FPL% / PTC / net premium / income tax / total
        cost, plus the savings, cliff headroom, and any shortfall. Composes the healthcare and
        tax engines. Deterministic; not tax advice.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ACAOptimizeRequest"
            examples:
              early_retiree_2026:
                summary: Single 60-year-old, $60k spend, ample Roth (2026 cliff year)
                value:
                  tax_year: 2026
                  filing_status: single
                  age: 60
                  household_size: 1
                  benchmark_annual_premium: 11000
                  spending_need: 60000
                  taxable_balance: 300000
                  taxable_gain_fraction: 0.5
                  deferred_balance: 800000
                  roth_balance: 400000
      responses:
        "200":
          description: The optimized vs naive draw comparison
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ACAOptimizeResponse"
        "400":
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
        "405":
          description: Method not allowed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /v1/tax/liquidation-schedule:
    post:
      tags: [Tax]
      operationId: taxLiquidationSchedule
      summary: Concentrated-position liquidation optimizer (multi-year, tax-aware)
      description: >
        Schedules the year-by-year sale of a large low-basis position (founder/inherited stock,
        long-vested RSUs) to diversify without one huge capital-gains / NIIT hit. Each year it
        realizes gain up to a chosen objective — fill the 15% LTCG bracket, stay under the §1411
        NIIT threshold, or a fixed annual cap — pricing the 0/15/20 + NIIT tax incrementally
        through the tax engine (so it matches the tax harness, incl. a $0-income founder whose
        gain alone crosses the NIIT threshold). Returns the per-year schedule, years to
        diversify, and a lump-sum comparison with the tax saved by spreading. Deterministic; not
        tax advice.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/LiquidationScheduleRequest"
            examples:
              founder_stock_15pct_bracket:
                summary: $2M founder stock, $200k basis, filling the 15% LTCG bracket
                value:
                  tax_year: 2025
                  filing_status: single
                  market_value: 2000000
                  cost_basis: 200000
                  other_ordinary_income: 80000
                  objective: target_bracket
                  growth_rate: 0.05
                  horizon_years: 15
      responses:
        "200":
          description: The multi-year sale schedule + lump-sum comparison
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LiquidationScheduleResponse"
        "400":
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
        "405":
          description: Method not allowed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /v1/tax/qsbs-exclusion:
    post:
      tags: [Tax]
      operationId: taxQSBSExclusion
      summary: QSBS (§1202) qualified-small-business-stock gain exclusion
      description: >
        Computes the IRC §1202 exclusion on a sale of qualified small-business stock — the
        exclusion percentage by acquisition vintage (legacy 50/75/100% for stock held >5 years)
        or holding-period tier (OBBBA-amended, for stock acquired after 2025-07-04: 50% at ≥3
        years, 75% at ≥4, 100% at ≥5), the excluded and taxable gain under the greater-of-$10M
        ($15M OBBBA) or 10×-basis per-issuer cap, the 28%-rate tax on the included portion
        (capped at the incremental ordinary tax), NIIT on the taxable gain, the §57(a)(7) 7% AMT
        preference on partial exclusions, and the saving versus a plain sale. Statutory
        parameters are grounded to §1202; deterministic; not tax advice.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/QSBSExclusionRequest"
            examples:
              founder_100pct:
                summary: $8M gain, 2015 stock held 6 years — fully excluded
                value:
                  acquisition_date: "2015-06-01"
                  holding_period_years: 6
                  basis: 500000
                  gain: 8000000
                  gross_assets_at_issuance: 30000000
                  filing_status: single
                  tax_year: 2025
                  other_income: 200000
      responses:
        "200":
          description: The exclusion, tax on the included gain, and the saving vs a plain sale
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/QSBSExclusionResponse"
        "400":
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
        "405":
          description: Method not allowed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /v1/equity-comp/nua:
    post:
      tags: [Equity compensation]
      operationId: equityCompNUA
      summary: Net unrealized appreciation (NUA) vs IRA rollover
      description: >
        Compares the two ways to take employer stock out of a qualified plan in a lump-sum
        distribution: the NUA election (ordinary tax on the cost basis now, long-term capital
        gains on the appreciation at sale) versus an IRA rollover (everything ordinary at
        withdrawal). Returns the tax under each strategy (nominal and present-value), the PV
        saving, a recommendation, and the holding period at which the rollover's deferral
        overtakes NUA. Caller supplies the marginal rates, so there is no year-keyed data;
        deterministic; not tax advice.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/NUARequest"
            examples:
              low_basis_stock:
                summary: $500k employer stock, $50k basis — NUA wins outright
                value:
                  filing_status: mfj
                  tax_year: 2025
                  cost_basis: 50000
                  market_value: 500000
                  marginal_ordinary_rate: 0.37
                  ltcg_rate: 0.20
                  growth_rate: 0.06
                  discount_rate: 0.05
      responses:
        "200":
          description: The NUA-vs-rollover comparison, saving, and breakeven
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/NUAResponse"
        "400":
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
        "405":
          description: Method not allowed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /v1/tax/s-corp-salary:
    post:
      tags: [Tax]
      operationId: taxSCorpSalary
      summary: S-corp reasonable-comp / salary optimizer
      description: >
        Finds the S-corporation owner-employee W-2 salary that maximizes after-tax income,
        trading the payroll tax on wages (6.2% OASDI to the year's wage base + 2.9% Medicare +
        0.9% Additional Medicare) against the §199A pass-through deduction's 50%-of-W-2-wages
        limit and the reasonable-compensation floor. Each candidate salary is priced through the
        QBI-aware tax engine (threshold phase-in, wage/UBIA limit, SSTB phase-out, the 20%-of-
        taxable cap, and the OBBBA minimum). Returns the recommended salary and its full
        breakdown, plus the all-wages baseline and the saving. The only year-keyed datum is the
        OASDI wage base (grounded to SSA). Deterministic; not tax advice.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/SCorpSalaryRequest"
            examples:
              high_income_owner:
                summary: $800k single-owner profit, $60k reasonable-comp floor
                value:
                  filing_status: single
                  tax_year: 2025
                  business_net_income: 800000
                  reasonable_comp_floor: 60000
      responses:
        "200":
          description: The recommended salary, its tax breakdown, and the saving vs all-wages
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SCorpSalaryResponse"
        "400":
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
        "405":
          description: Method not allowed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /v1/retirement/backdoor-roth:
    post:
      tags: [Retirement]
      operationId: retirementBackdoorRoth
      summary: Backdoor / mega-backdoor Roth (pro-rata rule)
      description: >
        Two Roth accumulation moves for earners over the direct-Roth income limits. The
        backdoor Roth applies the §408(d)(2) pro-rata rule: a nondeductible traditional-IRA
        contribution converted to Roth is taxed by basis across all traditional/SEP/SIMPLE IRAs,
        so a pre-tax balance makes part of the "backdoor" taxable (e.g. $93k pre-tax + $7k basis
        → 93% taxable). The mega-backdoor computes the after-tax 401(k) room under the §415(c)
        annual-additions limit (limit − elective deferrals − employer contributions, optionally
        capped by the plan). Returns the taxable/nontaxable split of the conversion, the basis
        and pre-tax left afterward, and the §415(c) room. Reuses the grounded contribution-limit
        tables; deterministic; not tax advice.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/BackdoorRothRequest"
            examples:
              pro_rata_trap:
                summary: $93k pre-tax IRA + a $7k nondeductible contribution (93% taxable)
                value:
                  filing_status: single
                  tax_year: 2025
                  age: 40
                  pretax_ira_balance: 93000
                  nondeductible_contribution: 7000
                  elective_deferrals: 23500
                  employer_contributions: 10000
      responses:
        "200":
          description: The pro-rata split of the conversion + the mega-backdoor room
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/BackdoorRothResponse"
        "400":
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
        "405":
          description: Method not allowed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /v1/retirement/qlac:
    post:
      tags: [Retirement income]
      operationId: retirementQLAC
      summary: Qualified longevity annuity contract (QLAC)
      description: >
        Models a deferred income annuity bought inside a traditional IRA. The premium — up to the
        §1.401(a)(9)-6(q)(2)(ii) year-keyed limit ($210,000 for 2025–26) — is carved out of the
        balance used for Required Minimum Distributions, lowering RMDs during the deferral, and
        buys guaranteed lifetime income starting no later than age 85. Returns the eligible/excess
        premium, the annual RMD reduction (drawn at the SECURE 2.0 beginning age on the supplied
        balance), and the deferred income from the start age (priced on the SSA mortality table),
        with the expected lifetime payout and money's-worth ratio. Composes the RMD + annuity
        engines; deterministic; not tax advice or an insurance quote.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/QLACRequest"
            examples:
              defer_to_85:
                summary: $210k QLAC in a $1M IRA for a 72-year-old, income from 85
                value:
                  tax_year: 2025
                  age: 72
                  birth_year: 1953
                  account_balance: 1000000
                  qlac_premium: 210000
                  start_age: 85
                  sex: male
                  assumed_rate: 0.045
      responses:
        "200":
          description: The RMD reduction + the deferred lifetime income the QLAC buys
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/QLACResponse"
        "400":
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
        "405":
          description: Method not allowed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /v1/usage:
    get:
      tags: [Platform]
      operationId: getUsage
      summary: Read the calling key's usage
      description: >
        Returns the total number of metered API calls made by the authenticated
        key plus its most recent usage events. One usage event is recorded per
        authenticated call.
      responses:
        "200":
          description: Usage summary
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/UsageResponse"
        "401":
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "503":
          description: >
            Usage records are temporarily unavailable (code `usage_unavailable`). This is NOT a
            report of zero usage — the store could not be reached. Retry shortly; do not treat
            it as a billing figure.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /v1/signup:
    post:
      tags: [Platform]
      operationId: signup
      summary: Get a free API key (self-serve)
      description: >
        Public endpoint (no API key required). Solve the Cloudflare Turnstile
        challenge in the browser, post the resulting token here, and receive a
        free, rate-limited key that expires in 30 days. The secret is returned
        once and is never recoverable — store it securely. Returns 501 until the
        feature is enabled on the server.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/SignupRequest"
            examples:
              getFreeKey:
                summary: A Turnstile token from the browser widget
                value:
                  turnstile_token: "XXXX.DUMMY.TOKEN.XXXX"
                  email: "you@example.com"
      responses:
        "200":
          description: A new free key (shown once) and its limits
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SignupResponse"
        "400":
          description: Missing token or the Turnstile challenge failed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "429":
          description: Too many signups from your network today
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "501":
          description: Self-serve signup is not enabled on this server
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "503":
          description: The daily free-key limit has been reached
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: "Send the API key as 'Authorization: Bearer <key>'."
    apiKey:
      type: apiKey
      in: header
      name: X-API-Key
      description: "Alternatively, send the API key in the 'X-API-Key' header."
  responses:
    # Shared error responses that (almost) every authenticated endpoint can return.
    # $ref'd from each operation so generated clients model them uniformly.
    Unauthorized:
      description: Missing or invalid API key.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    RateLimited:
      description: The key's per-minute request budget is exceeded. Retry after the delay in the Retry-After header.
      headers:
        Retry-After:
          description: Seconds to wait before retrying.
          schema: { type: integer }
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    Unavailable:
      description: A dependency (e.g. the key store) is temporarily unavailable, or the request exceeded its compute deadline. Retryable.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
  schemas:
    SignupRequest:
      type: object
      required:
        - turnstile_token
      properties:
        turnstile_token:
          type: string
          description: "The Cloudflare Turnstile response token produced by the browser widget."
          example: "XXXX.DUMMY.TOKEN.XXXX"
        email:
          type: string
          description: "Optional. Only used to label the key for support; not verified."
          example: "you@example.com"
    SignupResponse:
      type: object
      properties:
        key:
          type: string
          description: "The secret API key. Shown once; store it securely — it cannot be recovered."
          example: "nmr_sk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
        key_id:
          type: string
          example: "key_ab12cd34ef56ab12cd34ef56ab12cd34"
        plan:
          type: string
          example: "free"
        expires_at:
          type: string
          format: date-time
          example: "2026-07-27T12:00:00Z"
        limits:
          type: object
          properties:
            requests_per_min: { type: integer, example: 15 }
            daily_quota: { type: integer, example: 250 }
            max_simulations: { type: integer, example: 2000 }
        disclaimer:
          type: string
    MonteCarloRequest:
      type: object
      required:
        - current_age
        - retirement_age
        - end_age
        - current_balance
        - annual_contribution
        - annual_spending
      properties:
        current_age: { type: integer, minimum: 0, maximum: 120, example: 40 }
        retirement_age: { type: integer, example: 65, description: "Must be >= current_age." }
        end_age: { type: integer, maximum: 130, example: 95, description: "Planning horizon; must be > retirement_age." }
        current_balance: { type: number, minimum: 0, example: 250000 }
        annual_contribution: { type: number, minimum: 0, example: 20000, description: "Nominal contribution per year while working." }
        annual_spending: { type: number, minimum: 0, example: 70000, description: "Annual withdrawal in today's dollars; inflated over time." }
        expected_return: { type: number, minimum: -1, maximum: 1, example: 0.06, description: "Arithmetic mean nominal annual return (> -1, <= 1). Required unless glide_path is given." }
        return_volatility: { type: number, minimum: 0, maximum: 1, example: 0.12, description: "Stddev of nominal annual return (0–1). Required unless glide_path is given." }
        inflation: { type: number, default: 0.025, minimum: -1, maximum: 1, example: 0.025, description: "Annual inflation (> -1, <= 1)." }
        num_simulations: { type: integer, default: 10000, maximum: 200000, example: 10000 }
        annual_fee_rate:
          type: number
          format: double
          minimum: 0
          maximum: 0.10
          example: 0.01
          description: >-
            All-in annual cost drag as a decimal (0.01 = 1%): advisory fee plus fund expense
            ratios. Applied as a reduction to the expected return each year, so it compounds
            inside the return distribution rather than acting as a separate withdrawal.
            Omitted or 0 means projections are GROSS of fees, which is the historical
            behaviour and leaves result_id unchanged. Costs matter: on the example above a 1%
            fee moves success probability by roughly 14 points.
        seed:
          type: integer
          format: int64
          description: "Optional RNG seed. If omitted, one is generated and returned so the run can be reproduced."
          example: 12345
        glide_path:
          type: array
          maxItems: 64
          description: >
            Optional age-varying return assumptions. The engine linearly
            interpolates expected_return/return_volatility between waypoints (held
            flat before the first and after the last), instead of the fixed scalar
            fields. Waypoints must be in strictly ascending age order. When
            provided, expected_return/return_volatility are not required.
          items:
            type: object
            required: [age, expected_return, return_volatility]
            properties:
              age: { type: integer, minimum: 0, maximum: 130, example: 40 }
              expected_return: { type: number, minimum: -1, maximum: 1, example: 0.09 }
              return_volatility: { type: number, minimum: 0, maximum: 1, example: 0.18 }
        assets:
          type: array
          maxItems: 12
          description: >
            Optional correlated multi-asset projection. Each asset draws a correlated
            lognormal return (via a Cholesky of `correlations`) and the portfolio
            combines them by weight each year. When provided it supersedes the scalar
            and glide_path return inputs (and is mutually exclusive with glide_path);
            expected_return/return_volatility are then not required. A single asset at
            weight 1.0 reproduces the scalar run exactly. Weights must sum to 1.
          items:
            type: object
            required: [weight, expected_return, return_volatility]
            properties:
              class: { type: string, example: "us-equity", description: "Free-form label (the engine treats assets positionally)." }
              weight: { type: number, minimum: 0, maximum: 1, example: 0.6 }
              expected_return: { type: number, minimum: -1, maximum: 1, example: 0.08 }
              return_volatility: { type: number, minimum: 0, maximum: 1, example: 0.18 }
        correlations:
          type: array
          description: >
            Asset-return correlation matrix (len(assets) square, symmetric, unit
            diagonal, entries in [-1,1], positive semi-definite). Required for 2+
            assets; for a single asset an omitted matrix means the identity.
          items:
            type: array
            items: { type: number, minimum: -1, maximum: 1 }
          example: [[1, 0.2], [0.2, 1]]
        rebalance:
          type: boolean
          default: true
          description: >
            For the assets path: annual rebalancing to target weights (true, the
            default) vs buy-and-hold drift (false). Ignored without `assets`.
        guaranteed_income:
          type: array
          description: >
            Optional guaranteed-income streams (Social Security, pension, annuity) that
            offset retirement spending. Each retirement year the portfolio draw is
            max(0, spending − active streams). A stream may begin after retirement (the
            "retire at 60, claim Social Security at 67" bridge), so the portfolio
            correctly carries the full load during the gap years. Income only offsets
            the decumulation draw; surplus is not reinvested. Omit for the classic run.
          maxItems: 16
          items:
            type: object
            required: [annual_amount, start_age]
            properties:
              annual_amount: { type: number, minimum: 0, description: "Today's dollars.", example: 40000 }
              start_age: { type: integer, minimum: 0, maximum: 130, description: "First age the stream pays (inclusive).", example: 67 }
              end_age: { type: integer, minimum: 0, maximum: 130, description: "Last age it pays (inclusive); omit for life." }
              cola: { type: boolean, default: true, description: "Inflation-adjust the amount (default true). false = fixed nominal, real value erodes." }
        spending_rule:
          type: object
          description: >
            Optional dynamic-spending rule that replaces fixed inflation-adjusted spending
            with Guyton-Klinger guardrails (Guyton & Klinger 2006): each retirement year,
            cut spending if the withdrawal rate has risen above initial×(1+guard_upper),
            raise it if below initial×(1−guard_lower), and skip the inflation bump after a
            down year while spending exceeds the initial rate. Composes with
            guaranteed_income; not supported with the multi-asset model. Omit for fixed
            spending (byte-identical to the classic run).
          required: [type]
          properties:
            type: { type: string, enum: [guyton_klinger], description: "Rule family (only Guyton-Klinger today)." }
            initial_rate: { type: number, minimum: 0, exclusiveMaximum: 1, description: "Reference withdrawal rate the guardrails measure against. Omit (0) to derive it per path from the first retirement year's gross-spending rate." }
            guard_upper: { type: number, exclusiveMinimum: 0, exclusiveMaximum: 1, default: 0.20, description: "Capital-preservation band as a fraction of initial_rate." }
            guard_lower: { type: number, exclusiveMinimum: 0, exclusiveMaximum: 1, default: 0.20, description: "Prosperity band as a fraction of initial_rate." }
            cut: { type: number, exclusiveMinimum: 0, exclusiveMaximum: 1, default: 0.10, description: "Proportional spending cut when capital preservation trips." }
            raise: { type: number, exclusiveMinimum: 0, exclusiveMaximum: 1, default: 0.10, description: "Proportional spending raise when prosperity trips." }
            final_years_frozen: { type: integer, minimum: 0, default: 15, description: "Suspend capital-preservation cuts in the last N years of the plan." }
    MonteCarloResponse:
      type: object
      properties:
        result_id:
          type: string
          example: mc_1a2b3c4d5e6f7a8b
          description: "Deterministic id for this run (hash of inputs + seed + engine version)."
        engine_version: { type: string, example: montecarlo-1.0.0 }
        seed: { type: integer, format: int64, example: 12345 }
        inputs: { $ref: "#/components/schemas/MonteCarloRequest" }
        result:
          type: object
          properties:
            success_probability:
              type: number
              example: 0.89
              description: "Fraction of simulated paths whose money lasts to end_age."
            median_ending_balance: { type: number, example: 13100000 }
            ending_balance_percentiles:
              type: object
              properties:
                p10: { type: number }
                p25: { type: number }
                p50: { type: number }
                p75: { type: number }
                p90: { type: number }
            median_depletion_age:
              type: [integer, "null"]
              description: "Median age money runs out among failed paths; omitted if none failed."
            num_simulations: { type: integer, example: 10000 }
            spending:
              type: object
              description: "Present only when spending_rule was supplied: realized spending under the rule, in today's (real) dollars."
              properties:
                realized:
                  type: object
                  description: "Distribution of each path's mean annual real spending."
                  properties:
                    p10: { type: number }
                    p25: { type: number }
                    p50: { type: number }
                    p75: { type: number }
                    p90: { type: number }
                floor: { type: number, description: "10th percentile of per-path minimum real spending (a bad sequence's worst year)." }
                ceiling: { type: number, description: "90th percentile of per-path maximum real spending (a good sequence's best year)." }
                mean_cuts: { type: number, description: "Average capital-preservation cuts per path." }
                mean_raises: { type: number, description: "Average prosperity raises per path." }
        notices:
          type: array
          items: { type: string }
          description: >
            Optional advisories about how the request was handled — e.g. a free-tier
            key that omitted num_simulations is told the count was defaulted to the cap.
            Absent when there's nothing to flag.
        disclaimer: { type: string }
    TVMRequest:
      type: object
      required:
        - solve_for
      properties:
        solve_for:
          type: string
          enum: [fv, pv, pmt, rate, nper]
          description: "Which variable to solve for. The other four must be supplied."
        rate: { type: number, minimum: -1, maximum: 100, description: "Interest rate per period (> -1, magnitude <= 100). Required unless solving for rate.", example: 0.0033333333 }
        nper: { type: number, minimum: 0, maximum: 100000, description: "Number of periods (> 0, <= 100000). Required unless solving for nper.", example: 360 }
        pmt: { type: number, default: 0, description: "Payment per period (negative = outflow)." }
        pv: { type: number, default: 0, description: "Present value (negative = outflow)." }
        fv: { type: number, default: 0, description: "Future value." }
        payments_at_beginning:
          type: boolean
          default: false
          description: "If true, payments occur at the start of each period (annuity due)."
    TVMResponse:
      type: object
      properties:
        result_id: { type: string, example: tvm_1a2b3c4d5e6f7a8b }
        engine_version: { type: string, example: tvm-1.0.0 }
        solve_for: { type: string, example: pmt }
        inputs: { $ref: "#/components/schemas/TVMRequest" }
        result:
          type: object
          description: "The full, consistent set of variables with the solved value filled in."
          properties:
            rate: { type: number }
            nper: { type: number }
            pmt: { type: number }
            pv: { type: number }
            fv: { type: number }
        disclaimer: { type: string }
    GoalFundingRequest:
      type: object
      required:
        - target_amount
        - years
        - annual_return
      properties:
        target_amount: { type: number, minimum: 0, example: 1000000, description: "Future amount to reach (nominal)." }
        current_savings: { type: number, minimum: 0, default: 0, example: 50000 }
        years: { type: number, minimum: 0, maximum: 200, example: 30, description: "Horizon in years (> 0, <= 200)." }
        annual_return: { type: number, minimum: -1, maximum: 1, example: 0.06, description: "Expected nominal annual return (> -1, <= 1)." }
        periods_per_year: { type: integer, default: 12, minimum: 1, maximum: 366, example: 12, description: "Contribution frequency (1–366)." }
        contributions_at_beginning: { type: boolean, default: false }
    GoalFundingResponse:
      type: object
      properties:
        result_id: { type: string, example: goal_1a2b3c4d5e6f7a8b }
        engine_version: { type: string, example: goalfunding-1.0.0 }
        inputs: { $ref: "#/components/schemas/GoalFundingRequest" }
        result:
          type: object
          properties:
            required_periodic_contribution: { type: number, description: "Saving needed each period to hit the target." }
            required_annual_contribution: { type: number }
            periods: { type: number }
            periods_per_year: { type: integer }
            total_contributions: { type: number }
            current_savings_future_value: { type: number, description: "What current savings alone grow to." }
            projected_value: { type: number }
            goal_already_funded: { type: boolean, description: "True if current savings alone reach the target." }
        disclaimer: { type: string }
    LoanRequest:
      type: object
      required:
        - principal
        - annual_rate
        - term_months
      properties:
        principal: { type: number, minimum: 0, maximum: 1000000000000, example: 300000, description: "Amount borrowed (<= 1e12)." }
        annual_rate: { type: number, minimum: 0, maximum: 4, example: 0.065, description: "Nominal APR (0–4, i.e. up to 400%)." }
        term_months: { type: integer, minimum: 1, maximum: 1200, example: 360 }
        extra_monthly_payment: { type: number, minimum: 0, default: 0, description: "Extra principal each month; shortens the term." }
        include_schedule: { type: boolean, default: false, description: "Return the full per-period schedule (can be large)." }
    LoanResponse:
      type: object
      properties:
        result_id: { type: string, example: loan_1a2b3c4d5e6f7a8b }
        engine_version: { type: string, example: loan-1.0.0 }
        inputs: { $ref: "#/components/schemas/LoanRequest" }
        result:
          type: object
          properties:
            monthly_payment: { type: number, description: "Level payment actually made (scheduled + extra)." }
            scheduled_payment: { type: number, description: "Level payment for the original term, no extra." }
            number_of_payments: { type: integer, description: "Actual payments to payoff (<= term if extra paid)." }
            total_interest: { type: number }
            total_paid: { type: number }
            schedule:
              type: array
              description: "Per-period rows; present only when include_schedule is true."
              items:
                type: object
                properties:
                  period: { type: integer }
                  payment: { type: number }
                  principal: { type: number }
                  interest: { type: number }
                  balance: { type: number }
        disclaimer: { type: string }
    TaxEstimateRequest:
      type: object
      required:
        - tax_year
        - filing_status
      properties:
        tax_year: { type: integer, example: 2026, description: "Supported: 2025, 2026." }
        filing_status:
          type: string
          enum: [single, mfj, mfs, hoh]
          description: "single, married filing jointly (mfj), married filing separately (mfs), head of household (hoh)."
        ordinary_income:
          type: number
          minimum: 0
          default: 0
          description: "Income taxed at ordinary rates: wages, interest, non-qualified dividends, taxable retirement distributions, short-term gains."
        qualified_income:
          type: number
          minimum: 0
          default: 0
          description: "Long-term capital gains plus qualified dividends, taxed at preferential rates."
        itemized_deductions:
          type: number
          minimum: 0
          default: 0
          description: "If greater than the standard deduction, used in its place."
        net_investment_income:
          type: number
          minimum: 0
          description: "Basis for the 3.8% NIIT. Defaults to qualified_income if omitted."
        wages:
          type: number
          minimum: 0
          default: 0
          description: "Earned income subject to the 0.9% Additional Medicare Tax. Not added to income; only sets the surtax basis."
        state_tax_rate:
          type: number
          minimum: 0
          maximum: 1
          default: 0
          description: "Flat state income-tax rate applied to AGI (an approximation; ignored when `state` is set)."
        state:
          type: string
          example: CA
          description: "Optional 2-letter state code or DC. Computes the real per-state tax (all 50 states + DC, 2025) instead of state_tax_rate, and adds combined figures."
        state_taxable_income:
          type: number
          minimum: 0
          description: "State taxable income (non-SS) before the state standard deduction; defaults to ordinary_income."
        social_security_income:
          type: number
          minimum: 0
          description: "Social Security income; taxed only by the 9 states that tax SS (with `state` set)."
        include_amt:
          type: boolean
          default: false
          description: "Compute the Alternative Minimum Tax (§55) and fold any AMT owed into total_tax. Off by default."
        amt_preference_items:
          type: number
          minimum: 0
          description: "Sum of AMT add-backs to AMTI: SALT deducted for regular tax, the ISO bargain element, private-activity-bond interest, etc. Used only when include_amt is set."
        include_qbi:
          type: boolean
          default: false
          description: "Compute the §199A Qualified Business Income deduction and reduce taxable income by it. Off by default."
        qbi:
          type: number
          minimum: 0
          description: "Net qualified business income from pass-through trades/businesses. Used only when include_qbi is set."
        qbi_w2_wages:
          type: number
          minimum: 0
          description: "W-2 wages paid by the business; basis for the wage/UBIA limit that phases in above the threshold (greater of 50% of wages, or 25% of wages + 2.5% of UBIA)."
        qbi_ubia:
          type: number
          minimum: 0
          description: "Unadjusted basis immediately after acquisition (UBIA) of qualified property; the second term of the wage/UBIA limit."
        qbi_is_sstb:
          type: boolean
          default: false
          description: "Specified service trade or business (health, law, consulting, etc.); the deduction phases out entirely above the range."
        qbi_reit_ptp_dividends:
          type: number
          minimum: 0
          description: "Qualified REIT dividends + publicly-traded-partnership income; deductible at 20% with no wage/UBIA limit and no SSTB restriction."
        include_credits:
          type: boolean
          default: false
          description: "Compute the §24 Child Tax Credit and §32 Earned Income Credit and fold them into total_tax (which may go negative — a net refund). Off by default. Eligibility (EITC age/residency/joint-filing rules) is the caller's responsibility."
        qualifying_children:
          type: integer
          minimum: 0
          maximum: 20
          description: "Number of children qualifying for the Child Tax Credit (under 17). Used only when include_credits is set."
        other_dependents:
          type: integer
          minimum: 0
          maximum: 20
          description: "Number of dependents qualifying for the $500 (nonrefundable) Credit for Other Dependents."
        eitc_qualifying_children:
          type: integer
          minimum: 0
          maximum: 20
          description: "Number of children under the EITC definition (broader than the CTC's — through age 18, or 23 if a student). Capped at 3 for the credit table. Earned income is approximated by wages."
        aged_blind_count:
          type: integer
          minimum: 0
          maximum: 4
          description: "Number of §63(f) additional-standard-deduction conditions: count 1 for each of the taxpayer/spouse who is 65+ and each who is blind (0–4). Increases the standard deduction only ($2,000 unmarried / $1,600 married in 2025); ignored when itemizing."
        include_itemized_limit:
          type: boolean
          default: false
          description: "Apply the §68 overall limitation on itemized deductions (OBBBA's 2/37 reduction, in force for 2026+; suspended in 2025). Off by default; only changes a 37%-bracket itemizer."
        salt_paid:
          type: number
          minimum: 0
          description: "State-and-local taxes included in itemized_deductions, used to apply the §164(b)(6) cap ($40,000 in 2025 / $40,400 in 2026, phasing down 30% above $500k/$505k MAGI to a $10,000 floor). itemized_deductions should be the gross total including the full uncapped SALT. Must not exceed itemized_deductions."
        charitable_contributions:
          type: number
          minimum: 0
          description: "Charitable-giving portion included in itemized_deductions, used to apply the OBBBA §170(b)(1)(I) 0.5%-of-AGI floor for tax years 2026+: the deduction is reduced by min(charitable, 0.5% × AGI), since only giving above 0.5% of AGI is deductible. Assumed to already respect the §170(b) percentage ceilings (60/30/20% of AGI) — a lump-sum total can't separate cash vs noncash gifts — and carryforward of floored amounts is not modeled. Must not exceed itemized_deductions; no effect before 2026."
        force_itemize:
          type: boolean
          default: false
          description: "Disable the standard-deduction floor so the deduction equals itemized_deductions even when below the standard. Models §63(c)(6)(A): a married-filing-separately taxpayer whose spouse itemizes may not take the standard deduction. Off by default (deduction = max(standard, itemized))."
        senior_count:
          type: integer
          minimum: 0
          maximum: 2
          description: "Number of taxpayers age 65+ claiming the §151(d)(5) senior deduction (OBBBA, 2025–2028); 2 only on a joint return. Adds $6,000 each on top of the standard/itemized deduction, phasing out 6% of MAGI above $75,000 ($150,000 MFJ). Not available to MFS."
        retirement_contributions:
          type: number
          minimum: 0
          description: "IRA/401(k)/etc. contributions eligible for the §25B Saver's Credit (used only with include_credits). Counted up to $2,000 per person ($4,000 MFJ); the nonrefundable credit is 50/20/10% of that by AGI bracket. Eligibility (not a full-time student or dependent, age 18+) is the caller's responsibility."
        aotc_student_expenses:
          type: array
          maxItems: 20
          items: { type: number, minimum: 0 }
          description: "§25A American Opportunity Credit: per-student qualified education expenses (one entry per student). Each yields 100% of the first $2,000 + 25% of the next $2,000 (max $2,500); 40% of the total is refundable. Shares the §25A MAGI phase-out ($80k–$90k single / $160k–$180k MFJ); not available to MFS. Used only with include_credits."
        llc_expenses:
          type: number
          minimum: 0
          description: "§25A Lifetime Learning Credit: total qualified expenses (20% of up to $10,000 = max $2,000 per return, nonrefundable; same phase-out as the AOTC)."
        dependent_care_expenses:
          type: number
          minimum: 0
          description: "§21 child- and dependent-care expenses. Applied at the applicable percentage (35%→20% by AGI in 2025; the OBBBA two-stage 50%→35%→20% in 2026), capped at $3,000 (1 qualifying individual) / $6,000 (2+). Nonrefundable; the earned-income limit is the caller's responsibility."
        dependent_care_count:
          type: integer
          minimum: 0
          maximum: 20
          description: "Number of qualifying individuals for the §21 dependent-care credit (sets the $3,000 vs $6,000 expense cap)."
        adoption_expenses:
          type: number
          minimum: 0
          description: "§23 qualified adoption expenses (capped at $17,280 in 2025 / $17,670 in 2026). The credit phases out over $40k of MAGI above $259,190/$265,080; the OBBBA refundable portion (up to $5,000/$5,120) is paid even with no tax liability."
        adoption_special_needs:
          type: boolean
          default: false
          description: "If true, the §23 credit is the full year maximum regardless of actual expenses (adoption of a child with special needs)."
    TaxEstimateResponse:
      type: object
      properties:
        result_id: { type: string, example: tax_1a2b3c4d5e6f7a8b }
        engine_version: { type: string, example: tax-1.0.0 }
        inputs: { $ref: "#/components/schemas/TaxEstimateRequest" }
        result:
          type: object
          properties:
            agi: { type: number, description: "Adjusted gross income (ordinary + qualified)." }
            deduction_used: { type: number }
            taxable_income: { type: number }
            ordinary_tax: { type: number, description: "Tax on the ordinary-rate portion." }
            capital_gains_tax: { type: number, description: "Tax on LTCG / qualified dividends." }
            federal_income_tax: { type: number, description: "ordinary_tax + capital_gains_tax." }
            niit: { type: number, description: "3.8% Net Investment Income Tax." }
            additional_medicare_tax: { type: number, description: "0.9% Additional Medicare Tax." }
            state_tax: { type: number, description: "Flat-rate state tax, or the real per-state tax when `state` is set." }
            total_tax: { type: number, description: "Federal + state." }
            marginal_ordinary_rate: { type: number, description: "Rate on the next ordinary dollar.", example: 0.22 }
            effective_rate: { type: number, description: "total_tax / agi." }
            state_name: { type: string, description: "Present when `state` is set." }
            state_marginal_rate: { type: number, description: "State rate on the next dollar (present when `state` is set)." }
            combined_marginal_rate: { type: number, description: "Federal ordinary + state marginal (present when `state` is set)." }
            amt_income: { type: number, description: "Alternative Minimum Taxable Income (present when include_amt is set)." }
            amt_exemption: { type: number, description: "AMT exemption after phase-out (present when include_amt is set)." }
            tentative_minimum_tax: { type: number, description: "Tentative minimum tax (present when include_amt is set)." }
            amt: { type: number, description: "AMT owed above the regular tax; already included in total_tax (present when include_amt is set and positive)." }
            qbi_deduction: { type: number, description: "§199A QBI deduction; already reflected in taxable_income and the tax figures (present when include_qbi is set and positive)." }
            itemized_deduction_reduction: { type: number, description: "§68 reduction of itemized deductions; already reflected in taxable_income and the tax figures (present when include_itemized_limit applies and positive)." }
            salt_reduction: { type: number, description: "§164(b)(6) disallowed SALT; already reflected in taxable_income and the tax figures (present when salt_paid is set and the cap binds)." }
            charity_floor_reduction: { type: number, description: "§170(b)(1)(I) 0.5%-of-AGI charitable giving disallowed (2026+); already reflected in taxable_income (present when charitable_contributions is set)." }
            senior_deduction: { type: number, description: "§151(d)(5) senior deduction applied; already reflected in taxable_income and the tax figures (present when senior_count is set and positive)." }
            child_tax_credit: { type: number, description: "§24 Child Tax Credit, nonrefundable portion applied to tax (present when include_credits is set and positive)." }
            additional_child_tax_credit: { type: number, description: "§24(d) refundable Additional Child Tax Credit (present when include_credits is set and positive)." }
            earned_income_credit: { type: number, description: "§32 Earned Income Credit, refundable (present when include_credits is set and positive)." }
            savers_credit: { type: number, description: "§25B Saver's Credit, nonrefundable (present when include_credits is set and positive)." }
            education_credit: { type: number, description: "§25A education credit realized (AOTC, incl. its 40% refundable portion, + LLC); present when include_credits is set and positive." }
            dependent_care_credit: { type: number, description: "§21 child- and dependent-care credit, nonrefundable (present when include_credits is set and positive)." }
            adoption_credit: { type: number, description: "§23 adoption credit realized (nonrefundable + refundable portions); present when include_credits is set and positive." }
            total_credits: { type: number, description: "Total credit actually applied (nonrefundable credits capped at tax liability + all refundable parts); already reflected in total_tax (present when include_credits is set and positive)." }
        disclaimer: { type: string }
    RMDRequest:
      type: object
      required:
        - age
        - account_balance
      properties:
        age: { type: integer, minimum: 0, maximum: 130, example: 73, description: "Owner's age at the end of the distribution year." }
        account_balance: { type: number, minimum: 0, example: 1000000, description: "Prior-year-end balance of tax-deferred accounts." }
        birth_year:
          type: integer
          example: 1953
          description: "Optional. Sets the SECURE 2.0 beginning age (73, or 75 if born 1960+). Defaults to a beginning age of 73 if omitted."
        table:
          type: string
          enum: [uniform, single, joint]
          description: "Optional distribution-period table. \"uniform\" (default) is the owner's own RMD; \"single\" is the Single Life Table for beneficiary/inherited accounts (not gated by the SECURE 2.0 beginning age); \"joint\" is the Joint and Last Survivor Table for an owner whose sole beneficiary is a spouse more than 10 years younger (requires second_age)."
        second_age:
          type: integer
          minimum: 0
          maximum: 130
          description: "The sole spousal beneficiary's age. Required when table=joint."
    RMDResponse:
      type: object
      properties:
        result_id: { type: string, example: rmd_1a2b3c4d5e6f7a8b }
        engine_version: { type: string, example: rmd-1.0.0 }
        inputs: { $ref: "#/components/schemas/RMDRequest" }
        result:
          type: object
          properties:
            required: { type: boolean, description: "Whether an RMD is required at this age." }
            beginning_age: { type: integer, description: "Age at which RMDs must begin.", example: 73 }
            distribution_period: { type: number, description: "Life-expectancy factor used (Uniform or Single Life depending on `table`); 0 if not yet applicable.", example: 26.5 }
            amount: { type: number, description: "The required minimum distribution." }
            table_used: { type: string, description: "The distribution-period table applied (\"uniform\" or \"single\"); present only when `table` is supplied." }
        disclaimer: { type: string }
    WithdrawalSequencingRequest:
      type: object
      required:
        - tax_year
        - filing_status
        - spending_need
        - strategy
      properties:
        tax_year: { type: integer, example: 2025 }
        filing_status: { type: string, enum: [single, mfj, mfs, hoh] }
        spending_need: { type: number, minimum: 0, example: 90000, description: "After-tax cash needed this year." }
        strategy:
          type: string
          enum: [conventional, bracket_fill, proportional]
          description: "conventional = taxable -> tax-deferred -> Roth; bracket_fill = tax-deferred up to a target bracket then Roth/taxable; proportional = pro-rata across accounts."
        bracket_ceiling:
          type: number
          minimum: 0
          maximum: 1
          example: 0.22
          description: "Target marginal ordinary rate to fill up to. Required for the bracket_fill strategy."
        taxable_balance: { type: number, minimum: 0, default: 0 }
        tax_deferred_balance: { type: number, minimum: 0, default: 0 }
        tax_free_balance: { type: number, minimum: 0, default: 0 }
        taxable_cost_basis_fraction:
          type: number
          minimum: 0
          maximum: 1
          default: 1
          description: "Share of a taxable withdrawal that is return of basis (untaxed); the rest is a long-term gain."
        other_ordinary_income: { type: number, minimum: 0, default: 0, description: "Other ordinary income this year (sets the tax baseline)." }
        other_qualified_income: { type: number, minimum: 0, default: 0, description: "Other long-term gains / qualified dividends this year." }
        state_tax_rate: { type: number, minimum: 0, maximum: 1, default: 0 }
        age: { type: integer, description: "Optional. If set, the year's RMD is taken from the tax-deferred account first." }
        birth_year: { type: integer, description: "Optional. Sets the SECURE 2.0 RMD beginning age." }
    WithdrawalSequencingResponse:
      type: object
      properties:
        result_id: { type: string, example: wd_1a2b3c4d5e6f7a8b }
        engine_version: { type: string, example: withdrawal-1.0.0 }
        inputs: { $ref: "#/components/schemas/WithdrawalSequencingRequest" }
        result:
          type: object
          properties:
            strategy: { type: string }
            withdrawals:
              type: object
              properties:
                taxable: { type: number }
                tax_deferred: { type: number }
                tax_free: { type: number }
            gross_withdrawal: { type: number, description: "Total pulled across all accounts." }
            rmd_amount: { type: number, description: "Forced tax-deferred distribution included above." }
            taxable_gain_realized: { type: number }
            ordinary_income_total: { type: number }
            qualified_income_total: { type: number }
            incremental_tax: { type: number, description: "Tax caused by the withdrawals." }
            total_tax: { type: number, description: "Full-year tax including other income." }
            after_tax_cash: { type: number, description: "Spendable cash delivered (gross minus incremental tax)." }
            shortfall: { type: number, description: "Unmet spending need if balances are insufficient." }
            surplus: { type: number, description: "Cash beyond the need (e.g. when an RMD forces more)." }
            marginal_ordinary_rate: { type: number }
        disclaimer: { type: string }
    UsageResponse:
      type: object
      properties:
        key_id: { type: string, example: key_ab12cd34 }
        count: { type: integer, description: "Total metered calls by this key.", example: 42 }
        recent:
          type: array
          description: "The key's most recent usage events (newest last)."
          items:
            type: object
            properties:
              key_id: { type: string }
              method: { type: string, example: POST }
              path: { type: string, example: /v1/tvm }
              status: { type: integer, example: 200 }
              timestamp: { type: string, format: date-time }
    WithdrawalStrategyRequest:
      type: object
      required:
        - current_age
        - end_age
        - current_balance
        - initial_rate
        - strategy
      properties:
        annual_fee_rate:
          type: number
          format: double
          minimum: 0
          maximum: 0.10
          example: 0.01
          description: >-
            All-in annual cost drag as a decimal (0.01 = 1%): advisory fee plus fund expense
            ratios, applied as a reduction to the expected return. Omitted or 0 means the
            projection is GROSS of fees, which leaves result_id unchanged. Costs compound: a
            1% load moves long-horizon outcomes by a large margin (see the Concepts page).
        current_age: { type: integer, minimum: 40, maximum: 120, example: 65, description: "Start of decumulation; spending begins now." }
        end_age: { type: integer, maximum: 130, example: 95 }
        current_balance: { type: number, exclusiveMinimum: 0, example: 1000000 }
        initial_rate: { type: number, exclusiveMinimum: 0, maximum: 1, example: 0.04, description: "First-year withdrawal rate (used by all strategies except vpw)." }
        strategy:
          type: string
          enum: [fixed_real, fixed_pct, guardrails, vpw, floor_and_ceiling]
          example: guardrails
        expected_return: { type: number, minimum: -1, maximum: 1, example: 0.05, description: "Required unless glide_path is given." }
        return_volatility: { type: number, minimum: 0, maximum: 1, example: 0.12, description: "Required unless glide_path is given." }
        inflation: { type: number, minimum: -1, maximum: 1, default: 0.025, example: 0.02 }
        num_simulations: { type: integer, minimum: 1, maximum: 10000, default: 2000 }
        seed: { type: integer, format: int64, description: "Optional; generated and echoed back if absent (for reproducibility)." }
        glide_path:
          type: array
          maxItems: 64
          description: "Optional age-varying return assumptions (same shape as the Monte Carlo endpoint)."
          items:
            type: object
            properties:
              age: { type: integer }
              expected_return: { type: number }
              return_volatility: { type: number }
        guard_width: { type: number, description: "guardrails: band half-width vs the initial rate (default 0.20)." }
        guard_adjust: { type: number, description: "guardrails: spending cut/raise step (default 0.10)." }
        vpw_real_return: { type: number, description: "vpw: assumed real return for the amortization factor (default 0.03)." }
        floor_mult: { type: number, description: "floor_and_ceiling: lower bound × initial real withdrawal (default 0.85)." }
        ceiling_mult: { type: number, description: "floor_and_ceiling: upper bound × initial real withdrawal (default 1.20)." }
    WithdrawalStrategyResponse:
      type: object
      properties:
        result_id: { type: string, example: wds_1a2b3c4d5e6f7a8b }
        engine_version: { type: string, example: withdrawal-strategy-1.0.0 }
        seed: { type: integer, format: int64 }
        inputs: { $ref: "#/components/schemas/WithdrawalStrategyRequest" }
        result:
          type: object
          properties:
            strategy: { type: string }
            success_probability: { type: number, description: "Fraction of paths that fund spending to end_age." }
            spending_volatility: { type: number, description: "Median across paths of the stddev of annual real-spending growth." }
            median_first_year_spending: { type: number, description: "Real (today's) dollars." }
            median_lifetime_spending: { type: number, description: "Median across paths of total real spending." }
            spending_path:
              type: array
              description: "Per-year real-spending percentiles."
              items:
                type: object
                properties:
                  age: { type: integer }
                  p10: { type: number }
                  p50: { type: number }
                  p90: { type: number }
            ending_balance:
              type: object
              description: "Ending-balance percentiles (nominal)."
              properties:
                p10: { type: number }
                p25: { type: number }
                p50: { type: number }
                p75: { type: number }
                p90: { type: number }
            num_simulations: { type: integer }
            seed: { type: integer, format: int64 }
        disclaimer: { type: string }
    RothConversionRequest:
      type: object
      required:
        - tax_year
        - filing_status
        - current_age
        - end_age
        - tax_deferred_balance
        - annual_return
        - target_bracket
      properties:
        tax_year: { type: integer, example: 2026, description: "Supported: 2025, 2026." }
        filing_status: { type: string, enum: [single, mfj, mfs, hoh] }
        current_age: { type: integer, minimum: 0, maximum: 130, example: 63 }
        end_age: { type: integer, maximum: 130, example: 90, description: "Projection horizon (inclusive); must be > current_age." }
        birth_year: { type: integer, example: 1963, description: "Optional; sets the SECURE 2.0 RMD beginning age." }
        tax_deferred_balance: { type: number, minimum: 0, example: 1500000 }
        roth_balance: { type: number, minimum: 0, default: 0, example: 100000 }
        other_ordinary_income: { type: number, minimum: 0, default: 0, example: 30000, description: "Annual ordinary income besides RMDs/conversions (real)." }
        annual_return: { type: number, minimum: -1, maximum: 1, example: 0.05, description: "Real annual return applied to both accounts." }
        target_bracket: { type: number, minimum: 0, maximum: 1, example: 0.24, description: "Marginal ordinary rate to fill each conversion year." }
        conversion_end_age: { type: integer, default: 0, example: 73, description: "Stop converting at/before this age (exclusive). 0 = convert through end_age." }
        state_tax_rate: { type: number, minimum: 0, maximum: 1, default: 0 }
        irmaa_tier_cap: { type: integer, minimum: 0, maximum: 5, description: "Optional. Cap each year's conversion so MAGI stays under the top of this Medicare IRMAA tier (0–5)." }
        pay_tax_from_external: { type: boolean, default: false, description: "Pay each year's conversion tax from a side taxable account instead of netting it from the conversion, so the full conversion lands in the Roth." }
        external_taxable_balance: { type: number, minimum: 0, description: "The side taxable account that funds the conversion tax. Required (> 0) when pay_tax_from_external is set." }
        external_return: { type: number, minimum: -1, maximum: 1, description: "Real after-tax return on the external account; set below annual_return to model taxable drag. Defaults to annual_return." }
    RothConversionResponse:
      type: object
      properties:
        result_id: { type: string, example: roth_1a2b3c4d5e6f7a8b }
        engine_version: { type: string, example: roth-conversion-1.0.0 }
        inputs: { $ref: "#/components/schemas/RothConversionRequest" }
        result:
          type: object
          properties:
            schedule:
              type: array
              description: "Per-year projection of the conversion path."
              items:
                type: object
                properties:
                  age: { type: integer }
                  other_ordinary_income: { type: number }
                  rmd: { type: number }
                  conversion: { type: number, description: "Gross amount moved out of tax-deferred." }
                  ordinary_income: { type: number }
                  tax: { type: number, description: "Total tax for the year." }
                  conversion_tax: { type: number, description: "Incremental tax caused by the conversion." }
                  marginal_rate: { type: number }
                  irmaa_tier: { type: integer, description: "Medicare IRMAA tier this year (0 if pre-Medicare)." }
                  irmaa_surcharge: { type: number, description: "Annual IRMAA surcharge this year (2-year MAGI lookback)." }
                  tax_deferred_end: { type: number }
                  roth_end: { type: number }
                  external_taxable_end: { type: number, description: "Side taxable account year-end balance; present only with pay_tax_from_external." }
            with_conversions: { $ref: "#/components/schemas/RothScenario" }
            baseline: { $ref: "#/components/schemas/RothScenario" }
            after_tax_value_gain:
              type: number
              description: "with_conversions.after_tax_ending_value minus baseline's; positive favors converting (illustrative, terminal-rate basis)."
        disclaimer: { type: string }
    RothScenario:
      type: object
      properties:
        total_converted: { type: number }
        total_tax: { type: number, description: "Cumulative income tax over the horizon." }
        total_irmaa_surcharge: { type: number, description: "Cumulative Medicare IRMAA surcharges (2-year lookback)." }
        ending_tax_deferred: { type: number, description: "Pre-tax dollars." }
        ending_roth: { type: number, description: "Tax-free dollars." }
        ending_external_taxable: { type: number, description: "Side taxable account ending balance; present only with pay_tax_from_external." }
        conversion_tax_paid_externally: { type: number, description: "Cumulative conversion tax drawn from the external account; present only with pay_tax_from_external." }
        after_tax_ending_value: { type: number, description: "Roth + tax-deferred*(1 - final marginal rate) + external taxable - cumulative IRMAA." }
    SocialSecurityRequest:
      type: object
      required:
        - birth_year
        - pia_monthly
        - longevity_age
      properties:
        birth_year: { type: integer, minimum: 1943, maximum: 2100, example: 1962, description: "Sets the Full Retirement Age." }
        pia_monthly: { type: number, minimum: 0, example: 2800, description: "Primary Insurance Amount — the monthly benefit at Full Retirement Age." }
        longevity_age: { type: integer, minimum: 63, maximum: 120, example: 92, description: "Life expectancy used for lifetime totals." }
        discount_rate: { type: number, minimum: 0, maximum: 1, default: 0, example: 0.02, description: "Real annual rate for present value (0 = undiscounted)." }
        claim_age: { type: integer, minimum: 62, maximum: 70, description: "Optional. Primary's actual claim age, used for the household and earnings-test math. 0/absent = FRA." }
        annual_earnings: { type: number, minimum: 0, description: "Optional. Annual earned income for the earnings test (benefits withheld while working before FRA)." }
        earnings_test_year: { type: integer, description: "Required when annual_earnings > 0. Selects the year-keyed exempt amounts (2025 or 2026)." }
        cola_rate: { type: number, minimum: 0, maximum: 1, description: "Optional annual COLA; grows benefits from the claim year (nominal projection). 0 = real-dollar default." }
        survivor_claim_age: { type: integer, minimum: 60, maximum: 70, description: "Optional. Age the surviving spouse claims the survivor benefit; before the survivor's FRA it is reduced (max 28.5% at 60). 0/absent = FRA (no reduction). Requires a spouse." }
        spouse:
          type: object
          description: "Optional. Adds a spousal/survivor household analysis."
          required: [pia_monthly, birth_year]
          properties:
            pia_monthly: { type: number, minimum: 0, description: "Spouse's own PIA (0 if they never worked)." }
            birth_year: { type: integer, minimum: 1943, maximum: 2100 }
            claim_age: { type: integer, minimum: 62, maximum: 70, description: "Spouse's claim age; 0/absent = FRA." }
            divorced: { type: boolean, description: "Ex-spouse (married 10+ years): entitled to the spousal benefit on the ex's PIA; combined household income is then reported as 0." }
    SocialSecurityResponse:
      type: object
      properties:
        result_id: { type: string, example: ss_1a2b3c4d5e6f7a8b }
        engine_version: { type: string, example: social-security-1.0.0 }
        inputs: { $ref: "#/components/schemas/SocialSecurityRequest" }
        result:
          type: object
          properties:
            full_retirement_age:
              type: object
              properties:
                years: { type: integer, example: 67 }
                months: { type: integer, example: 0 }
            options:
              type: array
              description: "One entry per claiming age 62–70."
              items:
                type: object
                properties:
                  claim_age: { type: integer }
                  monthly_benefit: { type: number }
                  annual_benefit: { type: number }
                  percent_of_pia: { type: number, description: "Benefit as a fraction of PIA (e.g. 0.70 at 62 for FRA 67)." }
                  lifetime_benefit: { type: number, description: "Nominal sum to longevity." }
                  lifetime_benefit_pv: { type: number, description: "Present value discounted to age 62." }
                  earnings_test_withholding: { type: number, description: "Annual benefit withheld by the earnings test for under-FRA years (0 unless annual_earnings is set and this option claims before FRA)." }
                  earnings_test_withholding_fra_year: { type: number, description: "Withholding in the calendar year FRA is reached ($1 per $3 over the higher exempt amount). Present only when earnings are set and this option claims before FRA." }
                  restored_monthly_at_fra: { type: number, description: "Monthly benefit after withheld months are credited back at FRA (the earnings test defers, not forfeits). Present only when something is withheld." }
                  months_restored: { type: integer, description: "Number of withheld months credited back at FRA." }
            optimal_claim_age: { type: integer, description: "Maximizes lifetime present value." }
            optimal_claim_age_undiscounted: { type: integer, description: "Maximizes nominal lifetime total." }
            breakeven_age_62_vs_70: { type: integer, description: "Age at which claiming at 70 overtakes claiming at 62 cumulatively (0 if never by 120)." }
            household:
              type: object
              description: "Spousal & survivor analysis; present only when a spouse is supplied."
              properties:
                primary_monthly: { type: number, description: "Primary's benefit at their claim age." }
                spouse_own_monthly: { type: number, description: "Spouse's own retirement benefit." }
                spouse_spousal_monthly: { type: number, description: "Spousal-benefit level (up to 50% of primary PIA, reduced for early claim)." }
                spouse_total_monthly: { type: number, description: "What the spouse receives: max(own, spousal)." }
                combined_monthly: { type: number, description: "Both benefits together (0 for a divorced ex-spouse — no shared household)." }
                survivor_monthly: { type: number, description: "Benefit the survivor keeps after the first death (the higher of the two), reduced if claimed before the survivor's FRA." }
                divorced: { type: boolean, description: "Whether the spouse is a divorced ex-spouse (independent entitlement)." }
                survivor_reduction_factor: { type: number, description: "Multiplier applied for an early survivor claim (present only when survivor_claim_age is set)." }
                survivor_monthly_at_fra: { type: number, description: "Unreduced survivor benefit at the survivor's FRA (present only when survivor_claim_age is set)." }
        disclaimer: { type: string }
    CMARequest:
      type: object
      required:
        - assets
      properties:
        assets:
          type: array
          minItems: 1
          maxItems: 50
          description: "Asset classes; weights must sum to 1.0."
          items:
            type: object
            required: [expected_return, volatility, weight]
            properties:
              name: { type: string, example: stocks }
              expected_return: { type: number, minimum: -1, maximum: 1, example: 0.08 }
              volatility: { type: number, minimum: 0, maximum: 1, example: 0.18 }
              weight: { type: number, minimum: 0, maximum: 1, example: 0.6 }
        correlation:
          type: array
          description: "Optional N×N correlation matrix (N = number of assets). If omitted, default_correlation is used for every pair."
          items:
            type: array
            items: { type: number, minimum: -1, maximum: 1 }
        default_correlation: { type: number, minimum: -1, maximum: 1, default: 0, description: "Pairwise correlation used when no matrix is given." }
    CMAResponse:
      type: object
      properties:
        result_id: { type: string, example: cma_1a2b3c4d5e6f7a8b }
        engine_version: { type: string, example: cma-1.0.0 }
        inputs: { $ref: "#/components/schemas/CMARequest" }
        result:
          type: object
          properties:
            expected_return: { type: number, description: "Weighted-mean portfolio return (feed to Monte Carlo expected_return)." }
            volatility: { type: number, description: "Portfolio standard deviation (feed to Monte Carlo return_volatility)." }
            variance: { type: number }
        disclaimer: { type: string }
    HealthcareRequest:
      type: object
      required: [coverage_year, household_size, magi]
      properties:
        coverage_year: { type: integer, enum: [2025, 2026], example: 2026 }
        household_size: { type: integer, minimum: 1, maximum: 20, example: 2 }
        magi: { type: number, minimum: 0, example: 70000 }
        benchmark_annual_premium: { type: number, minimum: 0, example: 16000, description: "Second-lowest Silver plan, annual (area/age specific)." }
        state: { type: string, description: "Optional 2-letter code. \"AK\" and \"HI\" use their higher poverty guidelines; any other value (or empty) uses the 48 contiguous states + DC." }
        age: { type: integer, minimum: 0, maximum: 130, description: "65+ switches to the Medicare line." }
        filing_status: { type: string, enum: [single, mfj, mfs, hoh], description: "For the Medicare IRMAA surcharge." }
    HealthcareResponse:
      type: object
      properties:
        result_id: { type: string, example: hc_1a2b3c4d5e6f7a8b }
        engine_version: { type: string, example: healthcare-1.0.0 }
        inputs: { $ref: "#/components/schemas/HealthcareRequest" }
        result:
          type: object
          properties:
            on_medicare: { type: boolean }
            aca:
              type: object
              description: "Present before Medicare age."
              properties:
                fpl_value: { type: number, description: "Household poverty guideline (region-specific for AK/HI)." }
                fpl_percent: { type: number }
                applicable_percent: { type: number }
                expected_annual_contribution: { type: number }
                subsidy: { type: number }
                net_annual_premium: { type: number }
                eligible: { type: boolean }
                above_cliff: { type: boolean, description: "At/above 400% FPL with no subsidy (2026)." }
            medicare:
              type: object
              description: "Present at/after Medicare age."
              properties:
                part_b_base_annual: { type: number }
                irmaa_annual_surcharge: { type: number }
                total_annual: { type: number }
        disclaimer: { type: string }
    AnnuityRequest:
      type: object
      required: [age, sex, assumed_rate, payout_type]
      description: "Provide a premium, a target_income, or both (both evaluates a quote's money's-worth)."
      properties:
        premium: { type: number, minimum: 0, example: 100000, description: "0 = solve from target_income." }
        target_income: { type: number, minimum: 0, description: "Annual; 0 = solve from premium." }
        age: { type: integer, minimum: 0, maximum: 119, example: 65 }
        sex: { type: string, enum: [male, female], example: male }
        assumed_rate: { type: number, exclusiveMinimum: -1, maximum: 1, example: 0.045, description: "Pricing interest rate." }
        start_age: { type: integer, description: "Defer payments to this age (QLAC); 0/absent = immediate." }
        payout_type: { type: string, enum: [life, period_certain, joint_survivor], example: life }
        period_certain_years: { type: integer, minimum: 1, maximum: 50, description: "Guaranteed years (period_certain)." }
        survivor_pct: { type: number, minimum: 0, maximum: 1, description: "Fraction the survivor receives (joint_survivor)." }
        second_age: { type: integer, minimum: 0, maximum: 119 }
        second_sex: { type: string, enum: [male, female] }
    AnnuityResponse:
      type: object
      properties:
        result_id: { type: string, example: ann_1a2b3c4d5e6f7a8b }
        engine_version: { type: string, example: annuity-1.0.0 }
        inputs: { $ref: "#/components/schemas/AnnuityRequest" }
        result:
          type: object
          properties:
            annual_income: { type: number }
            monthly_income: { type: number }
            premium: { type: number }
            apv_factor: { type: number, description: "Actuarial PV of $1/yr of income." }
            expected_payout: { type: number, description: "Expected total nominal income over the annuitant's life." }
            moneys_worth: { type: number, description: "PV(benefits)/premium; 1.0 = actuarially fair at the assumed rate." }
        disclaimer: { type: string }
    LongevityRequest:
      type: object
      required: [age, sex]
      properties:
        age: { type: integer, minimum: 0, maximum: 119, example: 65 }
        sex: { type: string, enum: [male, female], example: male }
        health_adjustment: { type: number, minimum: 0, maximum: 5, default: 1, description: "Frailty multiplier on death probabilities (<1 healthier, >1 frailer)." }
        second_age: { type: integer, minimum: 0, maximum: 119, description: "Second life (enables joint metrics)." }
        second_sex: { type: string, enum: [male, female] }
        second_health_adjustment: { type: number, minimum: 0, maximum: 5, default: 1 }
    LongevityResponse:
      type: object
      properties:
        result_id: { type: string, example: lng_1a2b3c4d5e6f7a8b }
        engine_version: { type: string, example: longevity-1.0.0 }
        inputs: { $ref: "#/components/schemas/LongevityRequest" }
        result:
          type: object
          properties:
            life_expectancy: { type: number, description: "Expected remaining years." }
            median_age_at_death: { type: integer }
            survival_curve:
              type: array
              items:
                type: object
                properties:
                  age: { type: integer }
                  probability: { type: number }
            milestones:
              type: array
              items:
                type: object
                properties:
                  age: { type: integer }
                  probability: { type: number }
            joint_life_expectancy: { type: number, description: "Years to the first death (only with a second life)." }
            last_survivor_life_expectancy: { type: number, description: "Years to the last death (only with a second life)." }
        disclaimer: { type: string }
    SurvivorRequest:
      type: object
      required: [tax_year, spouse_a, spouse_b]
      properties:
        tax_year: { type: integer, enum: [2025, 2026], example: 2025 }
        deceased_spouse: { type: string, enum: [a, b], default: a, description: "Which spouse dies first." }
        spouse_a: { $ref: "#/components/schemas/SurvivorSpouse" }
        spouse_b: { $ref: "#/components/schemas/SurvivorSpouse" }
        shared_ordinary_income: { type: number, minimum: 0, default: 0, description: "Income that continues to the survivor (portfolio, joint annuity)." }
        on_medicare: { type: boolean, default: false, description: "Report IRMAA surcharges in both scenarios." }
    SurvivorSpouse:
      type: object
      properties:
        social_security: { type: number, minimum: 0, description: "Annual SS benefit." }
        other_income: { type: number, minimum: 0, description: "The spouse's own ordinary income; stops at their death." }
    SurvivorScenarioLine:
      type: object
      properties:
        filing_status: { type: string }
        ordinary_income: { type: number }
        tax: { type: number }
        effective_rate: { type: number }
        marginal_rate: { type: number }
        irmaa_surcharge: { type: number, description: "Annual; 0 unless on Medicare." }
        after_tax_income: { type: number }
    SurvivorResponse:
      type: object
      properties:
        result_id: { type: string, example: surv_1a2b3c4d5e6f7a8b }
        engine_version: { type: string, example: survivor-1.0.0 }
        inputs: { $ref: "#/components/schemas/SurvivorRequest" }
        result:
          type: object
          properties:
            both_alive: { $ref: "#/components/schemas/SurvivorScenarioLine" }
            survivor: { $ref: "#/components/schemas/SurvivorScenarioLine" }
            social_security_lost: { type: number, description: "The smaller SS benefit, which stops." }
            income_drop: { type: number }
            tax_change: { type: number, description: "Survivor tax minus both-alive tax." }
            after_tax_income_drop: { type: number }
        disclaimer: { type: string }
    PensionRequest:
      type: object
      required: [current_age, lump_sum_offer, monthly_pension, discount_rate, end_age]
      properties:
        current_age: { type: integer, minimum: 0, maximum: 120, example: 65 }
        lump_sum_offer: { type: number, exclusiveMinimum: 0, example: 300000 }
        monthly_pension: { type: number, exclusiveMinimum: 0, example: 2000 }
        cola_rate: { type: number, minimum: 0, maximum: 1, default: 0, description: "Annual pension increase." }
        discount_rate: { type: number, exclusiveMinimum: -1, maximum: 1, example: 0.05 }
        end_age: { type: integer, maximum: 130, example: 90, description: "Primary longevity assumption." }
        survivor_pct: { type: number, minimum: 0, maximum: 1, description: "Joint-and-survivor: fraction continued after end_age." }
        survivor_end_age: { type: integer, maximum: 130, description: "Survivor longevity (>= end_age)." }
    PensionResponse:
      type: object
      properties:
        result_id: { type: string, example: pen_1a2b3c4d5e6f7a8b }
        engine_version: { type: string, example: pension-1.0.0 }
        inputs: { $ref: "#/components/schemas/PensionRequest" }
        result:
          type: object
          properties:
            pension_present_value: { type: number, description: "PV of the pension stream at discount_rate." }
            lump_sum_offer: { type: number }
            net_advantage_annuity: { type: number, description: "PV(pension) - lump (>0 favors the annuity)." }
            recommendation: { type: string, enum: [annuity, lump_sum] }
            implied_return: { type: number, description: "Discount rate at which the two are equal (omitted if none)." }
            breakeven_age: { type: integer, description: "Age cumulative nominal pension overtakes the lump (0 if never)." }
        disclaimer: { type: string }
    AccumulationRequest:
      type: object
      required: [accounts, years]
      properties:
        annual_fee_rate:
          type: number
          format: double
          minimum: 0
          maximum: 0.10
          example: 0.01
          description: >-
            All-in annual cost drag as a decimal (0.01 = 1%): advisory fee plus fund expense
            ratios, applied as a reduction to the expected return. Omitted or 0 means the
            projection is GROSS of fees, which leaves result_id unchanged. Costs compound: a
            1% load moves long-horizon outcomes by a large margin (see the Concepts page).
        accounts:
          type: array
          minItems: 1
          maxItems: 20
          items:
            type: object
            required: [name]
            properties:
              name: { type: string }
              current_balance: { type: number, minimum: 0, default: 0 }
              annual_contribution: { type: number, minimum: 0, default: 0 }
              receives_match: { type: boolean, description: "The account the employer match is deposited into (default: the first)." }
              contribution_cap: { type: number, minimum: 0, default: 0, description: "Caps the escalated annual contribution (the 'escalate to a limit' mode); 0 = uncapped." }
        escalation_rate: { type: number, minimum: 0, maximum: 1, default: 0, description: "Annual % increase in contributions." }
        employer_match_rate: { type: number, minimum: 0, maximum: 10, default: 0, description: "Match on total employee contributions." }
        employer_match_limit: { type: number, minimum: 0, default: 0, description: "Annual $ cap on the match (0 = uncapped)." }
        expected_return: { type: number, minimum: -1, maximum: 1, example: 0.06, description: "Required unless a glide_path is supplied." }
        return_volatility: { type: number, minimum: 0, maximum: 1, description: "For the optional stochastic bands." }
        inflation: { type: number, minimum: -1, maximum: 1, default: 0.025 }
        years: { type: integer, minimum: 1, maximum: 100, example: 30 }
        num_simulations: { type: integer, minimum: 0, maximum: 50000, default: 0, description: "0 = deterministic only." }
        seed: { type: integer, format: int64, description: "Optional; generated and echoed when simulating." }
        current_age: { type: integer, minimum: 0, maximum: 120, description: "Anchors the glide path: the return for projection year y is taken at age current_age+y. Required when glide_path is supplied." }
        glide_path:
          type: array
          maxItems: 64
          description: >
            Optional age-varying return assumptions (same waypoint model as the
            Monte Carlo engine), overriding the scalar expected_return/
            return_volatility. Strictly ascending age order; when provided,
            expected_return is not required.
          items:
            type: object
            required: [age, expected_return, return_volatility]
            properties:
              age: { type: integer, minimum: 0, maximum: 130, example: 40 }
              expected_return: { type: number, minimum: -1, maximum: 1 }
              return_volatility: { type: number, minimum: 0, maximum: 1 }
    AccumulationResponse:
      type: object
      properties:
        result_id: { type: string, example: acc_1a2b3c4d5e6f7a8b }
        engine_version: { type: string, example: accumulation-1.0.0 }
        inputs: { $ref: "#/components/schemas/AccumulationRequest" }
        result:
          type: object
          properties:
            accounts:
              type: array
              items:
                type: object
                properties:
                  name: { type: string }
                  ending_balance: { type: number }
                  total_contributed: { type: number }
                  employer_match: { type: number }
            ending_total: { type: number, description: "Nominal." }
            ending_total_real: { type: number, description: "Deflated to today's dollars." }
            total_employee_contributions: { type: number }
            total_employer_match: { type: number }
            percentiles:
              type: object
              description: "Stochastic ending-total bands (only when num_simulations > 0)."
              properties:
                p10: { type: number }
                p25: { type: number }
                p50: { type: number }
                p75: { type: number }
                p90: { type: number }
            num_simulations: { type: integer }
            seed: { type: integer, format: int64 }
        disclaimer: { type: string }
    InheritedIRARequest:
      type: object
      required: [inherited_balance, account_growth_rate, tax_year, filing_status]
      properties:
        inherited_balance: { type: number, exclusiveMinimum: 0, example: 1000000 }
        account_growth_rate: { type: number, minimum: -1, maximum: 1, example: 0.06 }
        years: { type: integer, minimum: 1, maximum: 20, default: 10, description: "Distribution window (10 under SECURE)." }
        tax_year: { type: integer, enum: [2025, 2026], example: 2025 }
        filing_status: { type: string, enum: [single, mfj, mfs, hoh], example: single }
        other_income: { type: number, minimum: 0, default: 0, description: "Heir's annual ordinary income outside the IRA." }
        other_income_schedule: { type: array, items: { type: number }, description: "Optional per-year income (length = years); overrides other_income." }
        target_bracket: { type: number, minimum: 0, maximum: 1, default: 0.24, description: "Marginal rate the bracket_fill strategy fills up to." }
        annual_rmd_floor: { type: number, minimum: 0, description: "Minimum yearly distribution (e.g. when the decedent had begun RMDs)." }
    InheritedIRAResponse:
      type: object
      properties:
        result_id: { type: string, example: iira_1a2b3c4d5e6f7a8b }
        engine_version: { type: string, example: inherited-ira-1.0.0 }
        inputs: { $ref: "#/components/schemas/InheritedIRARequest" }
        result:
          type: object
          properties:
            gross_future_value: { type: number, description: "balance·(1+r)^N (same for every schedule)." }
            recommended: { type: string, enum: [even, bracket_fill, defer, immediate] }
            tax_savings_vs_worst: { type: number, description: "FV-of-tax gap between the best and worst schedule." }
            strategies:
              type: array
              items:
                type: object
                properties:
                  name: { type: string }
                  total_tax: { type: number, description: "Nominal cumulative tax." }
                  future_value_of_taxes: { type: number }
                  terminal_after_tax_wealth: { type: number }
                  schedule:
                    type: array
                    items:
                      type: object
                      properties:
                        year: { type: integer }
                        beginning_balance: { type: number }
                        distribution: { type: number }
                        other_income: { type: number }
                        incremental_tax: { type: number }
                        marginal_rate: { type: number }
                        ending_balance: { type: number }
        disclaimer: { type: string }
    DebtInvestRequest:
      type: object
      required: [loan_balance, loan_annual_rate, loan_minimum_payment, extra_monthly, horizon_years, expected_return, return_volatility]
      properties:
        annual_fee_rate:
          type: number
          format: double
          minimum: 0
          maximum: 0.10
          example: 0.01
          description: >-
            All-in annual cost drag as a decimal (0.01 = 1%): advisory fee plus fund expense
            ratios, applied as a reduction to the expected return. Omitted or 0 means the
            projection is GROSS of fees, which leaves result_id unchanged. Costs compound: a
            1% load moves long-horizon outcomes by a large margin (see the Concepts page).
        loan_balance: { type: number, exclusiveMinimum: 0, example: 30000 }
        loan_annual_rate: { type: number, minimum: 0, maximum: 1, example: 0.18 }
        loan_minimum_payment: { type: number, exclusiveMinimum: 0, example: 350, description: "Monthly." }
        extra_monthly: { type: number, exclusiveMinimum: 0, example: 500, description: "Discretionary monthly amount to allocate." }
        horizon_years: { type: integer, minimum: 1, maximum: 40, example: 15 }
        expected_return: { type: number, minimum: -1, maximum: 1, example: 0.06, description: "Annual investment return (supply after-tax to compare a taxable account)." }
        return_volatility: { type: number, minimum: 0, maximum: 1, example: 0.15 }
        num_simulations: { type: integer, minimum: 1, maximum: 10000, default: 5000 }
        seed: { type: integer, format: int64, description: "Optional; generated and echoed back." }
        marginal_rate: { type: number, minimum: 0, maximum: 1, description: "For the after-tax debt rate (e.g. from /v1/tax/estimate)." }
        interest_deductible: { type: boolean, default: false, description: "Carry the debt at the after-tax rate (mortgage / student loan)." }
    DebtInvestResponse:
      type: object
      properties:
        result_id: { type: string, example: dvi_1a2b3c4d5e6f7a8b }
        engine_version: { type: string, example: debt-vs-invest-1.0.0 }
        seed: { type: integer, format: int64 }
        inputs: { $ref: "#/components/schemas/DebtInvestRequest" }
        result:
          type: object
          properties:
            after_tax_debt_rate: { type: number, description: "The guaranteed hurdle." }
            payoff_months_avalanche: { type: integer, description: "Months to clear the debt under pay_debt." }
            median_net_worth_pay_debt: { type: number }
            median_net_worth_invest: { type: number }
            median_advantage_pay_debt: { type: number, description: "median(pay_debt - invest); >0 favors paying debt." }
            prob_invest_wins: { type: number }
            recommendation: { type: string, enum: [pay_debt, invest] }
            net_worth_pay_debt: { $ref: "#/components/schemas/Percentiles" }
            net_worth_invest: { $ref: "#/components/schemas/Percentiles" }
            num_simulations: { type: integer }
            seed: { type: integer, format: int64 }
        disclaimer: { type: string }
    Percentiles:
      type: object
      properties:
        p10: { type: number }
        p25: { type: number }
        p50: { type: number }
        p75: { type: number }
        p90: { type: number }
    CollegeRequest:
      type: object
      required: [current_child_age, annual_cost_today, expected_return]
      properties:
        current_child_age: { type: integer, minimum: 0, maximum: 30, example: 5 }
        college_start_age: { type: integer, default: 18 }
        years_in_college: { type: integer, minimum: 1, maximum: 30, default: 4 }
        annual_cost_today: { type: number, exclusiveMinimum: 0, example: 30000, description: "Current annual all-in cost of the target school." }
        tuition_inflation: { type: number, minimum: -1, maximum: 1, default: 0.05, description: "Tuition-specific inflation (typically faster than CPI)." }
        current_savings: { type: number, minimum: 0, default: 0 }
        expected_return: { type: number, minimum: -1, maximum: 1, example: 0.06 }
    CollegeResponse:
      type: object
      properties:
        result_id: { type: string, example: col_1a2b3c4d5e6f7a8b }
        engine_version: { type: string, example: college-savings-1.0.0 }
        inputs: { $ref: "#/components/schemas/CollegeRequest" }
        result:
          type: object
          properties:
            years_until_college: { type: integer }
            total_cost_nominal: { type: number, description: "Sum of the inflated college-year costs." }
            required_savings_at_start: { type: number, description: "Nest egg needed when college begins." }
            projected_without_saving: { type: number, description: "FV of current savings, no new contributions." }
            funding_gap: { type: number }
            required_annual_saving: { type: number }
            required_monthly_saving: { type: number }
            on_track: { type: boolean }
            cost_schedule:
              type: array
              items:
                type: object
                properties:
                  child_age: { type: integer }
                  cost: { type: number }
        disclaimer: { type: string }
    SavingsTargetRequest:
      type: object
      required: [current_age, retirement_age, end_age, target_annual_income, expected_return]
      properties:
        annual_fee_rate:
          type: number
          format: double
          minimum: 0
          maximum: 0.10
          example: 0.01
          description: >-
            All-in annual cost drag as a decimal (0.01 = 1%): advisory fee plus fund expense
            ratios, applied as a reduction to the expected return. Omitted or 0 means the
            projection is GROSS of fees, which leaves result_id unchanged. Costs compound: a
            1% load moves long-horizon outcomes by a large margin (see the Concepts page).
        current_age: { type: integer, minimum: 0, maximum: 120, example: 40 }
        retirement_age: { type: integer, example: 65 }
        end_age: { type: integer, maximum: 130, example: 90 }
        target_annual_income: { type: number, exclusiveMinimum: 0, example: 80000, description: "Today's dollars." }
        other_income: { type: number, minimum: 0, default: 0, description: "SS/pension at retirement, today's dollars." }
        current_savings: { type: number, minimum: 0, default: 0 }
        expected_return: { type: number, minimum: -1, maximum: 1, example: 0.06, description: "Nominal." }
        inflation: { type: number, minimum: -1, maximum: 1, default: 0.025 }
        withdrawal_rate: { type: number, minimum: 0, maximum: 1, description: "Optional; adds a rule-of-thumb nest egg (gap / rate)." }
        success_probability_target: { type: number, minimum: 0, maximum: 1, description: "Optional. When set, runs a Monte Carlo search for the smallest annual saving meeting this success rate." }
        return_volatility: { type: number, minimum: 0, maximum: 1, description: "Real-return volatility for the Monte Carlo search." }
        num_simulations: { type: integer, minimum: 0, maximum: 50000, description: "Required (> 0) when success_probability_target is set." }
        seed: { type: integer, format: int64, description: "Optional; generated and echoed when a target is set." }
    SavingsTargetResponse:
      type: object
      properties:
        result_id: { type: string, example: svt_1a2b3c4d5e6f7a8b }
        engine_version: { type: string, example: savings-target-1.0.0 }
        inputs: { $ref: "#/components/schemas/SavingsTargetRequest" }
        result:
          type: object
          properties:
            income_gap: { type: number, description: "Annual income the portfolio must fund (today's dollars)." }
            required_nest_egg: { type: number, description: "PV of the income gap over retirement." }
            required_nest_egg_by_rule: { type: number, description: "income_gap / withdrawal_rate (only when set)." }
            required_annual_savings: { type: number }
            required_monthly_savings: { type: number }
            projected_without_saving: { type: number, description: "FV of current savings, no new contributions." }
            shortfall_vs_trajectory: { type: number, description: "Required nest egg minus the above (0 if on track)." }
            on_track: { type: boolean }
            monte_carlo:
              type: object
              description: "Present only when success_probability_target is set."
              properties:
                success_probability_target: { type: number }
                required_annual_savings_mc: { type: number, description: "Smallest annual saving whose simulated success rate meets the target." }
                required_monthly_savings_mc: { type: number }
                achieved_success_probability: { type: number, description: "Success rate at the reported saving." }
                num_simulations: { type: integer }
                seed: { type: integer, format: int64 }
        disclaimer: { type: string }
    HSARequest:
      type: object
      required: [tax_year, coverage, age]
      properties:
        tax_year: { type: integer, enum: [2025, 2026], example: 2026 }
        coverage: { type: string, enum: [self, family], example: family }
        age: { type: integer, minimum: 0, maximum: 130, example: 55, description: "55+ adds the catch-up." }
        months_eligible: { type: integer, minimum: 1, maximum: 12, default: 12, description: "HDHP-covered months (proration)." }
        use_last_month_rule: { type: boolean, default: false, description: "Eligible on Dec 1 grants the full-year limit." }
        current_balance: { type: number, minimum: 0, default: 0 }
        annual_contribution: { type: number, minimum: 0, default: 0 }
        expected_return: { type: number, minimum: -1, maximum: 1, default: 0 }
        years: { type: integer, minimum: 0, maximum: 80, default: 0 }
        marginal_rate: { type: number, minimum: 0, maximum: 1, description: "Optional; drives the tax-advantage illustration." }
    HSAResponse:
      type: object
      properties:
        result_id: { type: string, example: hsa_1a2b3c4d5e6f7a8b }
        engine_version: { type: string, example: hsa-1.0.0 }
        inputs: { $ref: "#/components/schemas/HSARequest" }
        result:
          type: object
          properties:
            contribution_limit: { type: number, description: "Full-year limit incl. 55+ catch-up." }
            allowed_contribution: { type: number, description: "After last-month rule / proration." }
            projected_balance: { type: number, description: "HSA, tax-free growth." }
            contribution_tax_savings: { type: number, description: "Up-front deduction value (only when marginal_rate set)." }
            projected_taxable_equivalent: { type: number, description: "Same saving in a taxable account (only when marginal_rate set)." }
            hsa_advantage: { type: number, description: "HSA minus taxable equivalent (only when marginal_rate set)." }
        disclaimer: { type: string }
    InflationAdjustRequest:
      type: object
      required: [amount, annual_rate, years, direction]
      properties:
        amount: { type: number, example: 100 }
        annual_rate: { type: number, exclusiveMinimum: -1, maximum: 1, example: 0.025, description: "Inflation / COLA rate." }
        years: { type: integer, minimum: 0, maximum: 200, example: 10 }
        direction: { type: string, enum: [to_nominal, to_real], example: to_nominal }
    InflationAdjustResponse:
      type: object
      properties:
        result_id: { type: string, example: infl_1a2b3c4d5e6f7a8b }
        engine_version: { type: string, example: inflation-adjust-1.0.0 }
        inputs: { $ref: "#/components/schemas/InflationAdjustRequest" }
        result:
          type: object
          properties:
            adjusted_amount: { type: number, description: "The amount at the requested year." }
            cumulative_factor: { type: number }
            schedule:
              type: array
              description: "Per-year stream, 0..years."
              items:
                type: object
                properties:
                  year: { type: integer }
                  factor: { type: number }
                  amount: { type: number }
        disclaimer: { type: string }
    NPVIRRRequest:
      type: object
      required: [rate]
      description: "Provide exactly one of cashflows or dated_cashflows."
      properties:
        rate: { type: number, exclusiveMinimum: -1, example: 0.10, description: "Discount rate for NPV: per-period (periodic) or annual (dated)." }
        cashflows:
          type: array
          maxItems: 4096
          description: "Periodic amounts at the end of periods 0, 1, 2, …"
          items: { type: number }
          example: [-1000, 500, 400, 300, 100]
        dated_cashflows:
          type: array
          maxItems: 4096
          description: "Amounts on calendar dates (XNPV/XIRR)."
          items:
            type: object
            required: [date, amount]
            properties:
              date: { type: string, example: "2025-01-01", description: "YYYY-MM-DD" }
              amount: { type: number }
    NPVIRRResponse:
      type: object
      properties:
        result_id: { type: string, example: npv_1a2b3c4d5e6f7a8b }
        engine_version: { type: string, example: npv-irr-1.0.0 }
        inputs: { $ref: "#/components/schemas/NPVIRRRequest" }
        result:
          type: object
          properties:
            dated: { type: boolean, description: "Dated (XNPV/XIRR) vs periodic." }
            npv: { type: number, description: "NPV at the given rate (XNPV in dated mode)." }
            irr: { type: number, description: "Solved IRR (annualized XIRR); omitted when no sign change exists." }
            irr_found: { type: boolean }
        disclaimer: { type: string }
    ContributionLimitsRequest:
      type: object
      required: [tax_year, age, compensation]
      properties:
        tax_year: { type: integer, enum: [2025, 2026], example: 2026 }
        age: { type: integer, minimum: 0, maximum: 130, example: 62 }
        compensation: { type: number, minimum: 0, example: 250000, description: "Earned income / plan compensation; caps the contribution maxima." }
        prior_year_wages: { type: number, minimum: 0, description: "Optional. Prior-year FICA wages, for the SECURE 2.0 §603 Roth catch-up mandate (2026+)." }
        hsa_coverage: { type: string, enum: [none, self, family], default: none }
        elective_deferral_ytd: { type: number, minimum: 0, description: "Optional. 401(k)-type deferrals so far this year, for remaining headroom." }
        ira_contribution_ytd: { type: number, minimum: 0 }
        hsa_contribution_ytd: { type: number, minimum: 0 }
        employer_match_rate: { type: number, minimum: 0, maximum: 10, description: "Optional. Employer matches this fraction of employee deferrals (e.g. 0.5)." }
        employer_match_cap_pct: { type: number, minimum: 0, maximum: 1, description: "Match applies up to this fraction of compensation (e.g. 0.06)." }
    ContributionLimitsResponse:
      type: object
      properties:
        result_id: { type: string, example: cl_1a2b3c4d5e6f7a8b }
        engine_version: { type: string, example: contribution-limits-1.0.0 }
        inputs: { $ref: "#/components/schemas/ContributionLimitsRequest" }
        result:
          type: object
          properties:
            elective_deferral_limit: { type: number, description: "§402(g) base, before catch-up." }
            catch_up_amount: { type: number, description: "Age-appropriate 401(k)-type catch-up." }
            catch_up_type: { type: string, enum: [none, standard, super_60_63] }
            employee_deferral_max: { type: number, description: "Elective + catch-up, capped at compensation." }
            dc_annual_additions_limit: { type: number, description: "§415(c): employee + employer, excl. catch-up (capped at compensation)." }
            ira_limit: { type: number, description: "Incl. age-50 catch-up, capped at compensation." }
            simple_deferral_limit: { type: number, description: "Incl. applicable SIMPLE catch-up, capped at compensation." }
            sep_max: { type: number, description: "min(25% of compensation, §415(c))." }
            sep_eligible: { type: boolean }
            hsa_limit: { type: number, description: "Incl. 55+ catch-up; 0 if no coverage." }
            catch_up_must_be_roth: { type: boolean, description: "SECURE 2.0 §603 (2026+)." }
            elective_deferral_headroom: { type: number, description: "Limit − YTD (floored at 0)." }
            ira_headroom: { type: number }
            hsa_headroom: { type: number }
            contribution_for_full_match: { type: number, description: "Employee deferral needed to capture the full employer match." }
            employer_match_max: { type: number, description: "Matched dollars at that contribution." }
            total_tax_advantaged_capacity: { type: number, description: "Employee deferral + match + IRA + HSA." }
        disclaimer: { type: string }
    IRAEligibilityRequest:
      type: object
      required: [tax_year, filing_status, magi, age]
      properties:
        tax_year: { type: integer, enum: [2025, 2026], example: 2026 }
        filing_status: { type: string, enum: [single, mfj, mfs, hoh], example: single }
        magi: { type: number, minimum: 0, example: 160000, description: "Modified adjusted gross income." }
        age: { type: integer, minimum: 0, maximum: 130, example: 50, description: "Sets the catch-up portion of the IRA limit." }
        covered_by_workplace_plan: { type: boolean, default: false, description: "Whether the contributor is an active participant in an employer plan." }
        spouse_covered: { type: boolean, default: false, description: "Relevant when the contributor is not covered but files jointly/separately with a covered spouse." }
        compensation: { type: number, minimum: 0, description: "Optional. Caps the contribution at earned income." }
        elective_deferral: { type: number, minimum: 0, description: "Optional. 401(k) deferral this year, for mega-backdoor capacity." }
        employer_contributions: { type: number, minimum: 0, description: "Optional. Employer 401(k) contributions, for mega-backdoor capacity." }
        plan_allows_mega_backdoor: { type: boolean, default: false, description: "Whether the plan permits after-tax contributions + in-plan Roth conversion." }
    IRAEligibilityResponse:
      type: object
      properties:
        result_id: { type: string, example: irae_1a2b3c4d5e6f7a8b }
        engine_version: { type: string, example: ira-eligibility-1.0.0 }
        inputs: { $ref: "#/components/schemas/IRAEligibilityRequest" }
        result:
          type: object
          properties:
            ira_limit: { type: number, description: "Base + age-50 catch-up, capped at compensation." }
            traditional_deductible: { $ref: "#/components/schemas/EligibilityLine" }
            roth_contribution: { $ref: "#/components/schemas/EligibilityLine" }
            backdoor_roth_applicable: { type: boolean, description: "Direct Roth is income-restricted, so the nondeductible-trad-IRA + conversion route applies." }
            mega_backdoor_capacity: { type: number, description: "After-tax 401(k) space (§415(c) − deferral − employer); 0 unless the plan permits it." }
        disclaimer: { type: string }
    EligibilityLine:
      type: object
      properties:
        status: { type: string, enum: [full, partial, none] }
        amount: { type: number, description: "Deductible (traditional) or allowed (Roth) dollars." }
        phase_out_low: { type: number, description: "0 if no phase-out applies." }
        phase_out_high: { type: number, description: "0 if no phase-out applies." }
    StateTaxRequest:
      type: object
      required: [state, tax_year, filing_status, state_taxable_income]
      properties:
        state: { type: string, example: CA, description: "2-letter code or DC. All 50 states + DC covered (2025)." }
        tax_year: { type: integer, enum: [2025], example: 2025 }
        filing_status: { type: string, enum: [single, mfj, mfs, hoh], example: single }
        state_taxable_income: { type: number, minimum: 0, example: 100000, description: "Non-SS, non-pension income, before the state standard deduction." }
        social_security_income: { type: number, minimum: 0, description: "Added to the base only for the 9 states that tax SS; the other 41 + DC exempt it." }
        pension_income: { type: number, minimum: 0, description: "Optional. Generic qualified retirement income (private pension / 401k / 403b / traditional IRA). The state's 2025 exclusion is applied; the taxable remainder is added to the base." }
        age: { type: integer, minimum: 0, maximum: 130, description: "Optional. Gates age-conditioned pension exclusions (e.g. GA 62/65, NY 59½, IA 55)." }
    StateTaxResponse:
      type: object
      properties:
        result_id: { type: string, example: st_1a2b3c4d5e6f7a8b }
        engine_version: { type: string, example: state-income-1.0.0 }
        inputs: { $ref: "#/components/schemas/StateTaxRequest" }
        result:
          type: object
          properties:
            state: { type: string, example: California }
            kind: { type: string, enum: [none, flat, graduated] }
            taxable_income: { type: number, description: "After the state standard deduction (where modeled)." }
            state_tax: { type: number }
            effective_rate: { type: number }
            marginal_rate: { type: number }
            social_security_taxed: { type: boolean, description: "Whether this state taxes Social Security benefits." }
            pension_excluded: { type: number, description: "Portion of pension_income the state excludes." }
            pension_taxable: { type: number, description: "Pension_income remainder added to the taxable base." }
        disclaimer: { type: string }
    IRMAARequest:
      type: object
      required:
        - irmaa_year
        - filing_status
        - magi
      properties:
        irmaa_year: { type: integer, example: 2026, description: "Benefit year. Supported: 2025, 2026. Uses MAGI from two years prior." }
        filing_status: { type: string, enum: [single, mfj, mfs, hoh], description: "hoh uses the individual thresholds." }
        magi: { type: number, minimum: 0, example: 280000, description: "Modified adjusted gross income (from the 2-year-lookback year)." }
    IRMAAResponse:
      type: object
      properties:
        result_id: { type: string, example: irmaa_1a2b3c4d5e6f7a8b }
        engine_version: { type: string, example: irmaa-1.0.0 }
        inputs: { $ref: "#/components/schemas/IRMAARequest" }
        result:
          type: object
          properties:
            tier: { type: integer, description: "0 = standard (no surcharge) … 5 = highest.", example: 1 }
            part_b_standard: { type: number, description: "Monthly standard Part B premium." }
            part_b_surcharge: { type: number, description: "Monthly Part B IRMAA." }
            part_d_surcharge: { type: number, description: "Monthly Part D IRMAA." }
            part_b_total: { type: number, description: "Standard + Part B surcharge." }
            monthly_surcharge: { type: number, description: "Part B + Part D surcharge." }
            annual_surcharge: { type: number, description: "12 × monthly surcharge." }
            headroom_to_next_tier: { type: number, description: "MAGI room before the next tier (0 at the top)." }
            top_tier: { type: boolean }
        disclaimer: { type: string }
    MedicareLEPRequest:
      type: object
      properties:
        year: { type: integer, description: "Benefit year; omit for 2026." }
        part_b_months_late: { type: integer, minimum: 0, maximum: 600, description: "Months eligible but not enrolled in Part B without qualifying coverage." }
        part_d_months_late: { type: integer, minimum: 0, maximum: 600, description: "Full months without creditable drug coverage (63+ day gaps)." }
        projection_years: { type: integer, minimum: 0, maximum: 40, description: "Years to project the lifetime cost; omit for 20." }
    MedicareLEPResponse:
      type: object
      properties:
        result_id: { type: string, example: medlep_1a2b3c4d5e6f7a8b }
        engine_version: { type: string, example: medicare-lep-1.0.0 }
        inputs: { $ref: "#/components/schemas/MedicareLEPRequest" }
        result:
          type: object
          properties:
            year: { type: integer }
            part_b_standard_premium: { type: number, description: "Monthly standard Part B premium (from the IRMAA tables)." }
            part_b_full_periods_late: { type: integer, description: "Full 12-month periods late." }
            part_b_monthly_penalty: { type: number }
            part_d_base_premium: { type: number, description: "National base beneficiary premium." }
            part_d_monthly_penalty: { type: number }
            combined_monthly_penalty: { type: number }
            annual_penalty: { type: number, description: "Combined × 12." }
            projection_years: { type: integer }
            lifetime_penalty: { type: number, description: "Annual × projection years." }
        disclaimer: { type: string }
    BracketRoomRequest:
      type: object
      required:
        - tax_year
        - filing_status
        - taxable_income
      properties:
        tax_year: { type: integer, example: 2026, description: "Supported: 2025, 2026." }
        filing_status: { type: string, enum: [single, mfj, mfs, hoh] }
        taxable_income: { type: number, minimum: 0, example: 120000, description: "Ordinary taxable income (after deductions)." }
    BracketRoomResponse:
      type: object
      properties:
        result_id: { type: string, example: bracket_1a2b3c4d5e6f7a8b }
        engine_version: { type: string, example: bracket-room-1.0.0 }
        inputs: { $ref: "#/components/schemas/BracketRoomRequest" }
        result:
          type: object
          properties:
            marginal_rate: { type: number, description: "Rate on the next ordinary dollar.", example: 0.22 }
            ordinary_room:
              type: array
              description: "Bands at/above the current income with a finite top (the 37% band is omitted)."
              items:
                type: object
                properties:
                  rate: { type: number }
                  top_of_bracket: { type: number, description: "Taxable income at the top of the band." }
                  room: { type: number, description: "top_of_bracket - taxable_income." }
            ltcg_zero_room: { type: number, description: "Long-term gain that would be taxed at 0%." }
            ltcg_fifteen_room: { type: number, description: "Additional long-term gain taxed at 15%." }
        disclaimer: { type: string }
    EquityCompISORequest:
      type: object
      required: [tax_year, filing_status, shares, strike_price, fmv_at_exercise]
      properties:
        tax_year: { type: integer, example: 2025, description: "Supported tax year (2025 or 2026)." }
        filing_status: { type: string, enum: [single, mfj, mfs, hoh], example: single }
        ordinary_income: { type: number, minimum: 0, example: 200000, description: "Other taxable income, to set the AMT baseline." }
        shares: { type: number, minimum: 0, example: 10000 }
        strike_price: { type: number, minimum: 0, example: 1 }
        fmv_at_exercise: { type: number, minimum: 0, example: 11 }
        other_amt_preference_items: { type: number, minimum: 0, description: "Any additional AMT add-backs the taxpayer already has (optional)." }
        sale_price: { type: number, minimum: 0, description: "Optional. If given, also returns the same-year disqualifying-disposition outcome (sell the shares now: ordinary income, no AMT)." }
    EquityCompISOResponse:
      type: object
      properties:
        result_id: { type: string, example: eqc-iso_1a2b3c4d5e6f7a8b }
        engine_version: { type: string, example: equity-comp-1.0.0 }
        inputs: { $ref: "#/components/schemas/EquityCompISORequest" }
        result:
          type: object
          properties:
            bargain_element: { type: number, description: "(fmv − strike) × shares; the AMT preference." }
            amt_without_exercise: { type: number, description: "AMT on the baseline income alone." }
            amt_with_exercise: { type: number, description: "AMT including the bargain-element preference." }
            incremental_amt: { type: number, description: "Extra tax caused by exercising." }
            amt_credit_carryforward: { type: number, description: "Minimum Tax Credit (§53) generally usable against regular tax in later years." }
            disqualifying_disposition:
              type: object
              description: "Present only when sale_price is given — the alternative to holding (no AMT, but ordinary income now)."
              properties:
                ordinary_income: { type: number, description: "The bargain element capped at the actual gain." }
                capital_gain: { type: number, description: "Short-term gain above the exercise FMV (negative ⇒ a capital loss)." }
        disclaimer: { type: string }
    EquityCompRSURequest:
      type: object
      required: [tax_year, filing_status, shares, fmv_at_vest]
      properties:
        tax_year: { type: integer, example: 2025, description: "Supported tax year (2025 or 2026)." }
        filing_status: { type: string, enum: [single, mfj, mfs, hoh], example: single }
        other_ordinary_income: { type: number, minimum: 0, example: 150000, description: "Wages/other income, to find the marginal rate on the vest." }
        shares: { type: number, minimum: 0, example: 1000 }
        fmv_at_vest: { type: number, minimum: 0, example: 50 }
    EquityCompRSUResponse:
      type: object
      properties:
        result_id: { type: string, example: eqc-rsu_1a2b3c4d5e6f7a8b }
        engine_version: { type: string, example: equity-comp-1.0.0 }
        inputs: { $ref: "#/components/schemas/EquityCompRSURequest" }
        result:
          type: object
          properties:
            ordinary_income: { type: number, description: "fmv × shares at vest." }
            supplemental_withholding: { type: number, description: "Flat 22% (37% above $1M of supplemental wages)." }
            marginal_rate: { type: number, description: "True marginal rate on the vest income.", example: 0.24 }
            estimated_tax_on_vest: { type: number, description: "Incremental federal tax from the vest income." }
            withholding_gap: { type: number, description: "estimated_tax − supplemental_withholding (positive ⇒ likely under-withheld)." }
            shares_sold_to_cover: { type: number, description: "Shares sold to cover the supplemental withholding." }
            net_shares: { type: number, description: "Shares retained after sell-to-cover." }
            per_share_basis: { type: number, description: "Cost basis per retained share (= fmv at vest)." }
        disclaimer: { type: string }
    EquityCompNSORequest:
      type: object
      required: [tax_year, filing_status, shares, strike_price, fmv_at_exercise]
      properties:
        tax_year: { type: integer, example: 2025, description: "Supported tax year (2025 or 2026)." }
        filing_status: { type: string, enum: [single, mfj, mfs, hoh], example: single }
        other_ordinary_income: { type: number, minimum: 0, example: 150000, description: "Wages/other income, to find the marginal rate." }
        shares: { type: number, minimum: 0, example: 1000 }
        strike_price: { type: number, minimum: 0, example: 5 }
        fmv_at_exercise: { type: number, minimum: 0, example: 25 }
    EquityCompNSOResponse:
      type: object
      properties:
        result_id: { type: string, example: eqc-nso_1a2b3c4d5e6f7a8b }
        engine_version: { type: string, example: equity-comp-1.0.0 }
        inputs: { $ref: "#/components/schemas/EquityCompNSORequest" }
        result:
          type: object
          properties:
            ordinary_income: { type: number, description: "(fmv − strike) × shares." }
            supplemental_withholding: { type: number, description: "Flat 22% (37% above $1M)." }
            marginal_rate: { type: number, description: "True marginal rate on the spread.", example: 0.24 }
            estimated_tax: { type: number, description: "Incremental federal tax from the spread income." }
            withholding_gap: { type: number, description: "estimated_tax − supplemental_withholding." }
            shares_sold_to_cover: { type: number }
            net_shares: { type: number }
            per_share_basis: { type: number, description: "= fmv at exercise." }
        disclaimer: { type: string }
    EquityCompESPPRequest:
      type: object
      required: [shares, offering_date_price, purchase_date_price, sale_price, discount_pct]
      properties:
        shares: { type: number, minimum: 0, example: 100 }
        offering_date_price: { type: number, minimum: 0, example: 10, description: "FMV at the offering/grant date." }
        purchase_date_price: { type: number, minimum: 0, example: 20, description: "FMV at the purchase date." }
        sale_price: { type: number, minimum: 0, example: 30 }
        discount_pct: { type: number, minimum: 0, maximum: 1, example: 0.15, description: "Plan discount (e.g. 0.15 for 15%); applied to the lower of the two prices (lookback)." }
    EquityCompESPPResponse:
      type: object
      properties:
        result_id: { type: string, example: eqc-espp_1a2b3c4d5e6f7a8b }
        engine_version: { type: string, example: equity-comp-1.0.0 }
        inputs: { $ref: "#/components/schemas/EquityCompESPPRequest" }
        result:
          type: object
          properties:
            purchase_price: { type: number, description: "Price paid per share after the lookback discount." }
            qualifying:
              type: object
              properties:
                ordinary_income: { type: number, description: "Lesser of the gain and the offering-date discount." }
                capital_gain: { type: number, description: "Remainder (long-term)." }
            disqualifying:
              type: object
              properties:
                ordinary_income: { type: number, description: "Bargain at purchase (FMV − price paid)." }
                capital_gain: { type: number, description: "Sale − purchase-date FMV (short/long per holding period)." }
        disclaimer: { type: string }
    EquityCompISOAMTCapacityRequest:
      type: object
      required: [tax_year, filing_status]
      properties:
        tax_year: { type: integer, example: 2025, description: "Supported tax year (2025 or 2026)." }
        filing_status: { type: string, enum: [single, mfj, mfs, hoh], example: single }
        ordinary_income: { type: number, minimum: 0, example: 100000, description: "Other taxable income (the AMT baseline)." }
        other_amt_preference_items: { type: number, minimum: 0, description: "Any AMT add-backs the taxpayer already has (optional)." }
        strike_price: { type: number, minimum: 0, example: 5, description: "Optional — with fmv_at_exercise, expresses capacity as shares." }
        fmv_at_exercise: { type: number, minimum: 0, example: 25 }
    EquityCompISOAMTCapacityResponse:
      type: object
      properties:
        result_id: { type: string, example: eqc-iso-cap_1a2b3c4d5e6f7a8b }
        engine_version: { type: string, example: equity-comp-1.0.0 }
        inputs: { $ref: "#/components/schemas/EquityCompISOAMTCapacityRequest" }
        result:
          type: object
          properties:
            bargain_element_capacity: { type: number, description: "Largest bargain element exercisable with $0 AMT." }
            shares_capacity: { type: number, description: "Capacity ÷ (fmv − strike), when both prices are given." }
            unbounded: { type: boolean, description: "True if no AMT triggers up to the search cap." }
        disclaimer: { type: string }
    EquityCompSection83bRequest:
      type: object
      required: [tax_year, filing_status, shares, fmv_at_grant, fmv_at_vest]
      properties:
        tax_year: { type: integer, example: 2025, description: "Supported tax year (2025 or 2026)." }
        filing_status: { type: string, enum: [single, mfj, mfs, hoh], example: single }
        other_ordinary_income: { type: number, minimum: 0, example: 150000 }
        shares: { type: number, minimum: 0, example: 1000 }
        price_paid: { type: number, minimum: 0, example: 1, description: "Strike or purchase price per share (often 0 for an RSA)." }
        fmv_at_grant: { type: number, minimum: 0, example: 1 }
        fmv_at_vest: { type: number, minimum: 0, example: 20 }
    EquityCompSection83bResponse:
      type: object
      properties:
        result_id: { type: string, example: eqc-83b_1a2b3c4d5e6f7a8b }
        engine_version: { type: string, example: equity-comp-1.0.0 }
        inputs: { $ref: "#/components/schemas/EquityCompSection83bRequest" }
        result:
          type: object
          properties:
            with_election:
              type: object
              description: "Recognize the spread at grant (§83(b) elected)."
              properties:
                ordinary_income: { type: number }
                estimated_tax: { type: number }
                per_share_basis: { type: number, description: "= fmv at grant." }
            without_election:
              type: object
              description: "Recognize the spread at vest (default)."
              properties:
                ordinary_income: { type: number }
                estimated_tax: { type: number }
                per_share_basis: { type: number, description: "= fmv at vest." }
            ordinary_income_shifted_to_capital: { type: number, description: "(fmv at vest − fmv at grant) × shares — moved from ordinary income to capital-gain treatment by electing." }
        disclaimer: { type: string }
    EquityCompISOScheduleRequest:
      type: object
      required: [tax_year, filing_status, total_shares, strike_price, fmv_at_start, annual_ordinary_income, horizon_years]
      properties:
        tax_year: { type: integer, example: 2025, description: "Bracket year (2025/2026), used for every projected year." }
        filing_status: { type: string, enum: [single, mfj, mfs, hoh], example: single }
        total_shares: { type: number, minimum: 0, example: 6000 }
        strike_price: { type: number, minimum: 0, example: 5 }
        fmv_at_start: { type: number, minimum: 0, example: 25 }
        annual_fmv_growth: { type: number, minimum: -1, maximum: 1, description: "Per-year FMV growth (default 0)." }
        annual_ordinary_income: { type: number, minimum: 0, example: 100000 }
        annual_income_growth: { type: number, minimum: -1, maximum: 1, description: "Per-year income growth (default 0)." }
        horizon_years: { type: integer, minimum: 1, maximum: 40, example: 6 }
        annual_amt_budget: { type: number, minimum: 0, example: 5000, description: "AMT willing to incur per year (default 0 = stay at the crossover)." }
    EquityCompISOScheduleResponse:
      type: object
      properties:
        result_id: { type: string, example: eqc-iso-sched_1a2b3c4d5e6f7a8b }
        engine_version: { type: string, example: equity-comp-1.0.0 }
        inputs: { $ref: "#/components/schemas/EquityCompISOScheduleRequest" }
        result:
          type: object
          properties:
            schedule:
              type: array
              items:
                type: object
                properties:
                  year: { type: integer }
                  fmv: { type: number }
                  shares_exercised: { type: number }
                  bargain_element: { type: number }
                  amt_incurred: { type: number }
                  mtc_recovered: { type: number, description: "§53 Minimum Tax Credit recovered this year." }
                  cumulative_shares: { type: number }
            total_amt_paid: { type: number }
            total_mtc_recovered: { type: number }
            net_amt_cost: { type: number, description: "AMT paid less MTC recovered within the horizon." }
            years_to_fully_exercise: { type: integer, description: "0 if not fully exercised within the horizon." }
            shares_unexercised: { type: number }
        disclaimer: { type: string }
    GainHarvestingRequest:
      type: object
      required: [tax_year, filing_status, taxable_income]
      properties:
        tax_year: { type: integer, example: 2025, description: "Supported tax year (2025 or 2026)." }
        filing_status: { type: string, enum: [single, mfj, mfs, hoh], example: single }
        taxable_income: { type: number, minimum: 0, example: 40000, description: "Ordinary taxable income (after deductions); sets where the gain stacks." }
        realize_amount: { type: number, minimum: 0, description: "Optional long-term gain to realize → returns the realization detail." }
        magi: { type: number, minimum: 0, description: "Optional modified AGI; adds the 3.8% NIIT on the part of the gain above the §1411 threshold." }
    GainHarvestingResponse:
      type: object
      properties:
        result_id: { type: string, example: gh_1a2b3c4d5e6f7a8b }
        engine_version: { type: string, example: gain-harvesting-1.0.0 }
        inputs: { $ref: "#/components/schemas/GainHarvestingRequest" }
        result:
          type: object
          properties:
            ltcg_zero_room: { type: number, description: "Long-term gain realizable at 0%." }
            ltcg_fifteen_room: { type: number, description: "Additional gain realizable at 15%." }
            realization:
              type: object
              description: "Present only when realize_amount is supplied."
              properties:
                amount_at_0: { type: number }
                amount_at_15: { type: number }
                amount_at_20: { type: number }
                ltcg_tax: { type: number }
                niit: { type: number, description: "3.8% on the part of the gain above the NIIT threshold (present when magi is given)." }
                total_tax: { type: number }
                marginal_rate: { type: number, description: "Rate on the next gain dollar (band rate + NIIT).", example: 0.15 }
        disclaimer: { type: string }
    TLHBenefitRequest:
      type: object
      required: [tax_year, filing_status, taxable_income, harvestable_loss, reinvestment_years]
      properties:
        tax_year: { type: integer, example: 2025, description: "Supported tax year (2025 or 2026)." }
        filing_status: { type: string, enum: [single, mfj, mfs, hoh], example: single }
        taxable_income: { type: number, minimum: 0, example: 200000, description: "Sets the marginal ordinary and LTCG rates." }
        harvestable_loss: { type: number, minimum: 0, example: 50000 }
        offsettable_gains: { type: number, minimum: 0, description: "Capital gains the loss can offset this year (default 0)." }
        reinvestment_years: { type: number, minimum: 0, maximum: 100, example: 10, description: "Horizon over which the deferral is valued." }
        growth_rate: { type: number, minimum: -0.9, maximum: 1, example: 0.07, description: "Discount/return rate for the deferral PV (default 0.07)." }
    TLHBenefitResponse:
      type: object
      properties:
        result_id: { type: string, example: tlh_1a2b3c4d5e6f7a8b }
        engine_version: { type: string, example: tlh-1.0.0 }
        inputs: { $ref: "#/components/schemas/TLHBenefitRequest" }
        result:
          type: object
          properties:
            marginal_rate: { type: number }
            ltcg_rate: { type: number }
            gains_offset: { type: number, description: "Loss applied against capital gains." }
            ordinary_offset: { type: number, description: "Loss applied against ordinary income (≤ $3,000)." }
            carryforward: { type: number }
            immediate_tax_saving: { type: number }
            future_tax_recapture: { type: number, description: "Tax owed later on the lowered basis." }
            net_present_value: { type: number, description: "Immediate saving − PV(recapture)." }
            wash_sale_caveat: { type: string }
        disclaimer: { type: string }
    AssetLocationRequest:
      type: object
      required: [tax_year, filing_status, taxable_balance, tax_advantaged_balance, assets]
      properties:
        tax_year: { type: integer, example: 2025, description: "Supported tax year (2025 or 2026)." }
        filing_status: { type: string, enum: [single, mfj, mfs, hoh], example: single }
        taxable_income: { type: number, minimum: 0, example: 200000, description: "Sets the marginal ordinary and LTCG rates." }
        taxable_balance: { type: number, minimum: 0, example: 100000 }
        tax_advantaged_balance: { type: number, minimum: 0, example: 100000, description: "Tax-deferred + Roth combined (the shelter)." }
        assets:
          type: array
          minItems: 1
          maxItems: 50
          description: "Holdings; must sum to taxable_balance + tax_advantaged_balance."
          items:
            type: object
            required: [name, amount, expected_return, yield]
            properties:
              name: { type: string, example: bonds }
              amount: { type: number, minimum: 0 }
              expected_return: { type: number, minimum: -1, maximum: 1, example: 0.04 }
              yield: { type: number, minimum: 0, maximum: 1, example: 0.04, description: "Annual income yield (dividends/interest)." }
              qualified_pct: { type: number, minimum: 0, maximum: 1, description: "Fraction of yield taxed at the LTCG rate (rest ordinary)." }
              turnover: { type: number, minimum: 0, maximum: 1, description: "Fraction of appreciation realized as gains each year." }
    AssetLocationResponse:
      type: object
      properties:
        result_id: { type: string, example: aloc_1a2b3c4d5e6f7a8b }
        engine_version: { type: string, example: asset-location-1.0.0 }
        inputs: { $ref: "#/components/schemas/AssetLocationRequest" }
        result:
          type: object
          properties:
            marginal_rate: { type: number }
            ltcg_rate: { type: number }
            placements:
              type: array
              items:
                type: object
                properties:
                  name: { type: string }
                  drag_rate: { type: number, description: "Annual tax per dollar if held in a taxable account." }
                  taxable_amount: { type: number }
                  tax_advantaged_amount: { type: number }
                  recommendation: { type: string, enum: [taxable, tax_advantaged, split] }
            annual_drag_prorata: { type: number, description: "Drag if each asset were spread pro-rata across accounts." }
            annual_drag_optimized: { type: number, description: "Drag under the recommended placement." }
            annual_drag_saved: { type: number }
        disclaimer: { type: string }
    SSTaxRequest:
      type: object
      required: [filing_status, social_security_benefits, other_income]
      properties:
        filing_status: { type: string, enum: [single, mfj, mfs, hoh], example: single, description: "mfs assumes lived-with-spouse (§86 $0 base); a separated filer should use single." }
        social_security_benefits: { type: number, minimum: 0, example: 24000, description: "Gross annual Social Security benefit." }
        other_income: { type: number, minimum: 0, example: 35000, description: "AGI excluding Social Security (pensions, IRA/401k distributions, wages, gains, etc.)." }
        tax_exempt_interest: { type: number, minimum: 0, example: 2000, description: "Optional. Added back into provisional income under §86(b)(2)." }
        marginal_ordinary_rate: { type: number, minimum: 0, maximum: 1, example: 0.22, description: "Optional. When set, the response reports the tax-torpedo effective marginal rate on the next dollar of other income." }
    SSTaxResponse:
      type: object
      properties:
        result_id: { type: string, example: sstax_1a2b3c4d5e6f7a8b }
        engine_version: { type: string, example: ss-tax-1.0.0 }
        inputs: { $ref: "#/components/schemas/SSTaxRequest" }
        result:
          type: object
          properties:
            provisional_income: { type: number, description: "other_income + tax_exempt_interest + ½ benefits." }
            base_1: { type: number, description: "§86 first threshold for the filing status." }
            base_2: { type: number, description: "§86 second threshold." }
            taxable_social_security: { type: number, description: "§86(a) taxable portion (≤ 85% of benefits)." }
            nontaxable_social_security: { type: number }
            taxable_percent: { type: number, description: "taxable / benefits (0…0.85)." }
            tier: { type: integer, description: "0 none · 1 50% band · 2 85% band · 3 capped at 85%." }
            marginal_inclusion_rate: { type: number, description: "§86 benefits pulled into tax by the next dollar of other income (0, 0.5, or 0.85)." }
            effective_marginal_rate: { type: number, description: "marginal_ordinary_rate × (1 + marginal_inclusion_rate); the tax torpedo. 0 if no rate supplied." }
        disclaimer: { type: string }
    MarginalMapRequest:
      type: object
      required: [tax_year, filing_status, sweep_start, sweep_stop, sweep_step]
      properties:
        tax_year: { type: integer, example: 2025 }
        filing_status: { type: string, enum: [single, mfj, mfs, hoh] }
        ordinary_income: { type: number, minimum: 0, description: "Base ordinary income held fixed (pensions, IRA distributions, wages, taxable interest)." }
        social_security_benefits: { type: number, minimum: 0, description: "Gross annual Social Security (drives the §86 torpedo)." }
        qualified_income: { type: number, minimum: 0, description: "LTCG + qualified dividends (also approximates net investment income for NIIT)." }
        tax_exempt_interest: { type: number, minimum: 0, description: "Muni interest; added back into §86 provisional income and into MAGI." }
        sweep_variable: { type: string, enum: [ordinary, ltcg], default: ordinary, description: "Which income component the swept dollars add to." }
        sweep_start: { type: number, minimum: 0 }
        sweep_stop: { type: number, description: "Must exceed sweep_start." }
        sweep_step: { type: number, description: "> 0. The grid is (stop−start)/step + 1 points (max 1000)." }
        include_irmaa: { type: boolean, default: false, description: "Add the Medicare Part B/D IRMAA surcharge. IRMAA is assessed on a 2-year MAGI lookback; the map shows the surcharge this MAGI triggers under the tax_year schedule." }
        medicare_beneficiaries: { type: integer, enum: [1, 2], default: 1, description: "Scales the per-beneficiary IRMAA surcharge (2 = a couple both enrolled)." }
        include_aca: { type: boolean, default: false, description: "Add the ACA premium-tax-credit net premium (pre-65 context)." }
        household_size: { type: integer, minimum: 1, description: "Required when include_aca." }
        benchmark_annual_premium: { type: number, minimum: 0, description: "Second-lowest-cost Silver plan, annual; required when include_aca." }
        state: { type: string, description: "FPL region for ACA: \"AK\" / \"HI\", or empty for the 48 contiguous states + DC." }
    MarginalMapResponse:
      type: object
      properties:
        result_id: { type: string, example: marginalmap_1a2b3c4d5e6f7a8b }
        engine_version: { type: string, example: marginal-map-1.0.0 }
        inputs: { $ref: "#/components/schemas/MarginalMapRequest" }
        result:
          type: object
          properties:
            points:
              type: array
              description: "One entry per grid point along the swept variable."
              items:
                type: object
                properties:
                  income: { type: number, description: "Swept-variable level (sweep_start + i·step)." }
                  agi: { type: number, description: "Resulting adjusted gross income." }
                  marginal_rate: { type: number, description: "True marginal rate on the next dollar of the swept variable." }
                  total_cost: { type: number, description: "Total annual cost C (income tax + NIIT + Additional Medicare + IRMAA + ACA net premium)." }
                  components:
                    type: object
                    description: "Additive breakdown of marginal_rate (the parts sum to it)."
                    properties:
                      ordinary: { type: number, description: "Ordinary bracket rate on the swept dollar." }
                      ss_inclusion: { type: number, description: "§86 torpedo: extra ordinary tax from more Social Security becoming taxable." }
                      ltcg: { type: number, description: "LTCG / qualified-dividend rate (0/15/20 stacking)." }
                      niit: { type: number, description: "3.8% NIIT + 0.9% Additional Medicare." }
                      irmaa_step: { type: number, description: "Amortized IRMAA surcharge change (spikes at a tier crossing)." }
                      aca_step: { type: number, description: "Amortized ACA subsidy change (spikes at the 400%-FPL cliff)." }
            cliffs:
              type: array
              description: "Discrete discontinuities — IRMAA tier crossings and the ACA 400%-FPL subsidy cliff."
              items:
                type: object
                properties:
                  income: { type: number, description: "Swept-variable level just past the cliff (located within one step)." }
                  kind: { type: string, enum: [irmaa, aca] }
                  annual_jump: { type: number, description: "Discrete annual dollars added at the cliff." }
        disclaimer: { type: string }
    CoupleClaimRequest:
      type: object
      required: [spouse_a, spouse_b]
      properties:
        spouse_a: { $ref: "#/components/schemas/ClaimSpouse" }
        spouse_b: { $ref: "#/components/schemas/ClaimSpouse" }
        discount_rate: { type: number, minimum: 0, maximum: 1, default: 0, description: "Real annual discount rate for the present value (benefits are real dollars; 0 = undiscounted)." }
    ClaimSpouse:
      type: object
      required: [birth_year, current_age, sex]
      properties:
        pia_monthly: { type: number, minimum: 0, description: "Monthly Primary Insurance Amount at Full Retirement Age (0 = never worked; spousal/survivor only)." }
        birth_year: { type: integer, description: "Determines Full Retirement Age." }
        current_age: { type: integer, description: "Drives mortality and the earliest claimable age (<= 70)." }
        sex: { type: string, enum: [male, female], description: "Mortality table." }
        health_adjustment: { type: number, minimum: 0, maximum: 5, default: 1, description: "Mortality scale: 1 = table, <1 healthier (longer), >1 frailer." }
    CoupleClaimResponse:
      type: object
      properties:
        result_id: { type: string, example: coupleclaim_1a2b3c4d5e6f7a8b }
        engine_version: { type: string, example: couple-claim-1.0.0 }
        inputs: { $ref: "#/components/schemas/CoupleClaimRequest" }
        result:
          type: object
          properties:
            optimal:
              type: object
              description: "The claim-age pair maximizing expected household PV."
              properties:
                label: { type: string, example: optimal }
                claim_age_a: { type: integer, example: 69 }
                claim_age_b: { type: integer, example: 62 }
                expected_pv: { type: number }
            comparisons:
              type: array
              description: "Reference strategies (both_earliest, both_fra, both_70) and their expected PV."
              items:
                type: object
                properties:
                  label: { type: string }
                  claim_age_a: { type: integer }
                  claim_age_b: { type: integer }
                  expected_pv: { type: number }
            surface:
              type: array
              description: "The full expected-PV grid (one cell per claim-age pair) — for a heatmap."
              items:
                type: object
                properties:
                  claim_age_a: { type: integer }
                  claim_age_b: { type: integer }
                  expected_pv: { type: number }
            fra_year_a: { type: integer, description: "Spouse A's Full Retirement Age (whole years)." }
            fra_year_b: { type: integer, description: "Spouse B's Full Retirement Age (whole years)." }
            gain_vs_latest: { type: number, description: "Optimal PV minus both-claim-at-70 PV." }
            gain_vs_fra: { type: number, description: "Optimal PV minus both-claim-at-FRA PV." }
        disclaimer: { type: string }
    QCDPlanRequest:
      type: object
      required: [tax_year, filing_status, age, ira_balance, intended_gift]
      properties:
        tax_year: { type: integer, enum: [2025, 2026], description: "QCD-supported years." }
        filing_status: { type: string, enum: [single, mfj, mfs, hoh] }
        age: { type: integer, minimum: 70, maximum: 120, description: "IRA owner's age (>= 70 for QCD eligibility, 70½)." }
        birth_year: { type: integer, description: "Optional; sets the SECURE 2.0 RMD beginning age (default 73)." }
        ira_balance: { type: number, minimum: 0, description: "Prior-year-end tax-deferred balance (drives the RMD)." }
        intended_gift: { type: number, minimum: 0, description: "Charitable gift amount; compared up to the QCD annual limit." }
        ordinary_income: { type: number, minimum: 0, description: "Pensions etc., excluding the IRA distribution." }
        social_security_benefits: { type: number, minimum: 0 }
        qualified_income: { type: number, minimum: 0, description: "LTCG + qualified dividends." }
        tax_exempt_interest: { type: number, minimum: 0 }
        other_itemized: { type: number, minimum: 0, description: "Non-charitable itemized deductions." }
        medicare_beneficiaries: { type: integer, enum: [1, 2], default: 1, description: "Scales the per-beneficiary IRMAA surcharge." }
    QCDPlanResponse:
      type: object
      properties:
        result_id: { type: string, example: qcd_1a2b3c4d5e6f7a8b }
        engine_version: { type: string, example: qcd-1.0.0 }
        inputs: { $ref: "#/components/schemas/QCDPlanRequest" }
        result:
          type: object
          properties:
            rmd: { type: number, description: "Required minimum distribution this year (0 if not yet required)." }
            rmd_required: { type: boolean }
            qcd_limit: { type: number, description: "The §408(d)(8) annual limit for the year." }
            qcd_amount: { type: number, description: "The gift compared (min of intended_gift and the limit)." }
            gift_capped: { type: boolean, description: "True if intended_gift exceeded the QCD limit." }
            qcd: { $ref: "#/components/schemas/QCDStrategy" }
            take_rmd_then_donate: { $ref: "#/components/schemas/QCDStrategy" }
            net_tax_saving: { type: number, description: "Take-RMD total cost minus QCD total cost (positive = QCD wins)." }
            magi_reduction: { type: number, description: "How much lower the QCD keeps MAGI." }
            irmaa_reduction: { type: number, description: "Annual IRMAA surcharge avoided by the QCD." }
            taxable_ss_reduction: { type: number, description: "Less Social Security pulled into tax." }
        disclaimer: { type: string }
    QCDStrategy:
      type: object
      description: "The tax picture under one giving approach (annual dollars)."
      properties:
        agi: { type: number }
        magi: { type: number, description: "AGI + tax-exempt interest (IRMAA basis)." }
        taxable_ss: { type: number, description: "§86 taxable Social Security." }
        income_tax: { type: number, description: "Federal income tax + NIIT + Additional Medicare." }
        irmaa_surcharge: { type: number, description: "Annual Part B/D IRMAA × beneficiaries." }
        total_cost: { type: number, description: "income_tax + irmaa_surcharge." }
    EstimatedTaxRequest:
      type: object
      required: [tax_year, filing_status, prior_year_tax, prior_year_agi, projected_current_tax, withholding]
      properties:
        tax_year: { type: integer, minimum: 2000, maximum: 2100, description: "Current tax year. Must be 2025 or 2026 unless underpayment_rate is supplied." }
        filing_status: { type: string, enum: [single, mfj, mfs, hoh] }
        prior_year_tax: { type: number, minimum: 0, description: "Total tax on the prior-year return (the prior-year safe-harbor base)." }
        prior_year_agi: { type: number, minimum: 0, description: "Prior-year AGI; over $150,000 ($75,000 MFS) raises the prior-year harbor to 110%." }
        projected_current_tax: { type: number, minimum: 0, description: "Expected total tax for the current year." }
        withholding: { type: number, minimum: 0, description: "Expected withholding this year (credited evenly across the four installments)." }
        estimated_payments:
          type: array
          items: { type: number, minimum: 0 }
          minItems: 4
          maxItems: 4
          description: "The four quarterly estimated payments (Q1..Q4); omit or zero any not yet paid."
        underpayment_rate: { type: number, minimum: 0, maximum: 1, description: "Optional; overrides the assumed annual IRS underpayment rate (e.g. 0.08). Omit to use the year's default (7% for 2025/2026)." }
    EstimatedTaxResponse:
      type: object
      properties:
        result_id: { type: string, example: esttax_1a2b3c4d5e6f7a8b }
        engine_version: { type: string, example: estimated-tax-1.0.0 }
        inputs: { $ref: "#/components/schemas/EstimatedTaxRequest" }
        result:
          type: object
          properties:
            current_year_safe_harbor: { type: number, description: "90% of projected current-year tax." }
            prior_year_safe_harbor: { type: number, description: "100% or 110% of prior-year tax." }
            prior_year_multiplier: { type: number, description: "1.00 or 1.10." }
            high_income: { type: boolean, description: "Prior-year AGI over the threshold." }
            required_annual_payment: { type: number, description: "The lesser of the two safe harbors." }
            governing_safe_harbor: { type: string, enum: [current_year_90pct, prior_year], description: "Which safe harbor binds." }
            total_paid: { type: number, description: "Withholding + all estimated payments." }
            shortfall: { type: number, description: "max(0, required_annual_payment − total_paid)." }
            safe_harbor_met: { type: boolean }
            required_quarterly: { type: number, description: "required_annual_payment / 4." }
            additional_per_quarter_needed: { type: number, description: "Ex-ante even estimated payment (beyond withholding) to reach the safe harbor: (required_annual_payment − withholding) / 4. It assumes no estimated payments yet, so it is not reduced by estimated_payments already made." }
            de_minimis: { type: boolean, description: "Balance due under $1,000 (§6654(e)(1))." }
            prior_year_exception: { type: boolean, description: "Prior year had zero tax (§6654(e)(2))." }
            installments:
              type: array
              items: { $ref: "#/components/schemas/EstimatedTaxInstallment" }
            penalty_rate: { type: number, description: "The assumed annual underpayment rate used." }
            rate_is_default: { type: boolean, description: "True if the year's grounded default was used." }
            penalty_applies: { type: boolean }
            estimated_penalty: { type: number, description: "The regular-method penalty estimate." }
        disclaimer: { type: string }
    EstimatedTaxInstallment:
      type: object
      description: "One quarterly period in the underpayment schedule."
      properties:
        quarter: { type: integer, description: "1..4." }
        due_date: { type: string, description: "MM-DD (the following year for Q4)." }
        required: { type: number, description: "25% of the required annual payment." }
        paid: { type: number, description: "withholding/4 + this quarter's estimated payment." }
        underpaid: { type: number, description: "Running cumulative underpayment (required-to-date − paid-to-date) at this due date; a later payment that catches the account up clears it (§6654(b)(3))." }
        penalty: { type: number, description: "Running underpayment × rate × this period's days / 365." }
        cumulative: { type: number, description: "Cumulative required amount through this quarter." }
    SurvivorTransitionRequest:
      type: object
      required: [tax_year, ordinary_income, social_security_higher, social_security_lower]
      properties:
        tax_year: { type: integer, enum: [2025, 2026], description: "Tables used for both scenarios." }
        ordinary_income: { type: number, minimum: 0, description: "Pensions, IRA/RMD withdrawals, interest — while both alive, excluding SS." }
        qualified_income: { type: number, minimum: 0, description: "LTCG + qualified dividends (portfolio rolls to the survivor)." }
        tax_exempt_interest: { type: number, minimum: 0, description: "Muni interest (added back for §86 + IRMAA MAGI)." }
        social_security_higher: { type: number, minimum: 0, description: "The larger annual benefit — the survivor keeps this." }
        social_security_lower: { type: number, minimum: 0, description: "The smaller — stops at the first death. Must not exceed social_security_higher." }
        survivor_ordinary_income: { type: number, minimum: 0, description: "Ordinary income continuing to the survivor. Omit to default to ordinary_income (accounts roll over, RMDs continue)." }
        conversion_amount: { type: number, minimum: 0, description: "Optional Roth conversion valued now (MFJ) vs as the survivor." }
    SurvivorTransitionResponse:
      type: object
      properties:
        result_id: { type: string, example: survtrans_1a2b3c4d5e6f7a8b }
        engine_version: { type: string, example: survivor-transition-1.0.0 }
        inputs: { $ref: "#/components/schemas/SurvivorTransitionRequest" }
        result:
          type: object
          properties:
            joint: { $ref: "#/components/schemas/SurvivorScenario" }
            survivor: { $ref: "#/components/schemas/SurvivorScenario" }
            annual_widow_penalty: { type: number, description: "Survivor total cost − joint total cost." }
            income_lost: { type: number, description: "Dropped SS benefit + lost ordinary income." }
            marginal_rate_jump: { type: number, description: "Survivor marginal rate − joint marginal rate." }
            conversion:
              type: object
              description: "Present only when conversion_amount > 0."
              properties:
                amount: { type: number }
                cost_now_mfj: { type: number, description: "Extra tax + IRMAA to convert while both alive." }
                cost_as_survivor: { type: number, description: "Extra tax + IRMAA to convert as the single survivor." }
                saving: { type: number, description: "cost_as_survivor − cost_now_mfj (positive = convert now)." }
        disclaimer: { type: string }
    SurvivorScenario:
      type: object
      description: "The tax picture under one filing status (annual dollars)."
      properties:
        filing_status: { type: string, enum: [mfj, single] }
        agi: { type: number }
        magi: { type: number, description: "AGI + tax-exempt interest (IRMAA basis)." }
        taxable_ss: { type: number, description: "§86 taxable Social Security." }
        income_tax: { type: number, description: "Federal income tax + NIIT + Additional Medicare." }
        irmaa: { type: number, description: "Annual Part B/D surcharge × beneficiaries." }
        total_cost: { type: number, description: "income_tax + irmaa." }
        marginal_rate: { type: number, description: "Federal marginal rate on the next ordinary dollar." }
        beneficiaries: { type: integer, description: "Medicare beneficiaries priced for IRMAA (2 joint, 1 survivor)." }
    SeppRequest:
      type: object
      required: [account_balance, age]
      properties:
        account_balance: { type: number, minimum: 0, description: "The balance the SEPP is calculated on." }
        age: { type: integer, minimum: 18, maximum: 80, description: "Current age (SEPP is for those under 59½)." }
        interest_rate: { type: number, minimum: 0, maximum: 0.10, description: "Amortization rate; omit for the 5% floor, or set 0 for the smallest compliant SEPP. Must not exceed the greater of 5% or 120% of the mid-term AFR (caller's responsibility)." }
    SeppResponse:
      type: object
      properties:
        result_id: { type: string, example: sepp_1a2b3c4d5e6f7a8b }
        engine_version: { type: string, example: sepp-1.0.0 }
        inputs: { $ref: "#/components/schemas/SeppRequest" }
        result:
          type: object
          properties:
            rmd_method: { $ref: "#/components/schemas/SeppMethod" }
            amortization_method: { $ref: "#/components/schemas/SeppMethod" }
            recommended: { $ref: "#/components/schemas/SeppMethod" }
            interest_rate: { type: number, description: "The rate used for amortization." }
            rate_is_default: { type: boolean, description: "True if the 5% floor was used." }
            minimum_duration_years: { type: number, description: "Longer of 5 years or until age 59½." }
            end_age: { type: number, description: "age + minimum_duration_years." }
            already_penalty_free: { type: boolean, description: "Age is already ≥ 59½ (SEPP unnecessary)." }
            bust_penalty_rate: { type: number, description: "0.10 (plus interest, not modeled)." }
            max_retroactive_penalty: { type: number, description: "Worst case: 10% of every scheduled distribution." }
        disclaimer: { type: string }
    SeppMethod:
      type: object
      description: "One SEPP calculation method and its annual payment."
      properties:
        name: { type: string, enum: [required_minimum_distribution, fixed_amortization] }
        annual_payment: { type: number }
        factor: { type: number, description: "The divisor: life-expectancy factor (RMD) or annuity factor (amortization)." }
        fixed: { type: boolean, description: "True if level for the whole schedule (false = recalculated yearly)." }
    LifeNeedsRequest:
      type: object
      required: [annual_income, income_replace_years]
      properties:
        annual_income: { type: number, minimum: 0, description: "The insured's gross annual income to replace." }
        income_replace_years: { type: integer, minimum: 1, maximum: 100, description: "Horizon for the income need." }
        income_replace_fraction: { type: number, minimum: 0, maximum: 1, description: "Share of income the survivors need. Omit for 0.70." }
        survivor_annual_income: { type: number, minimum: 0, description: "Income the survivor still earns/receives (reduces the gap)." }
        final_expenses: { type: number, minimum: 0, description: "Funeral + final medical." }
        debts: { type: number, minimum: 0, description: "Non-mortgage debts to clear." }
        mortgage_balance: { type: number, minimum: 0, description: "Mortgage payoff." }
        education_fund: { type: number, minimum: 0, description: "Lump sum for children's education." }
        emergency_fund: { type: number, minimum: 0, description: "Cash reserve for the survivors." }
        existing_assets: { type: number, minimum: 0, description: "Liquid assets available to survivors." }
        existing_life_insurance: { type: number, minimum: 0, description: "Coverage already in force." }
        real_return: { type: number, minimum: -0.1, maximum: 0.5, description: "Real rate for present-valuing the income need. Omit for 0.03, or set 0 for a conservative undiscounted need." }
        years_to_retirement: { type: integer, minimum: 0, maximum: 100, description: "Remaining working years; > 0 computes the human-life-value cross-check." }
        self_consumption_fraction: { type: number, minimum: 0, maximum: 1, description: "Share the insured consumes (HLV). Omit for 0.25." }
        effective_tax_rate: { type: number, minimum: 0, maximum: 1, description: "HLV uses after-tax income. Omit for gross (0)." }
    LifeNeedsResponse:
      type: object
      properties:
        result_id: { type: string, example: lifeneeds_1a2b3c4d5e6f7a8b }
        engine_version: { type: string, example: life-needs-1.0.0 }
        inputs: { $ref: "#/components/schemas/LifeNeedsRequest" }
        result:
          type: object
          properties:
            replacement_fraction_used: { type: number }
            annual_income_gap: { type: number, description: "Income to replace, net of survivor income." }
            income_replacement_need: { type: number, description: "Present value of the gap over the horizon." }
            real_return_used: { type: number }
            final_expenses: { type: number }
            debts: { type: number }
            mortgage_balance: { type: number }
            education_fund: { type: number }
            emergency_fund: { type: number }
            capital_need_total: { type: number, description: "Sum of all needs." }
            existing_resources: { type: number, description: "Assets + existing insurance." }
            additional_coverage_needed: { type: number, description: "The recommendation (>= 0)." }
            human_life_value: { type: number, description: "PV of future earnings net of self-consumption (0 without years_to_retirement)." }
        disclaimer: { type: string }
    DisabilityNeedsRequest:
      type: object
      required: [gross_annual_income]
      properties:
        gross_annual_income: { type: number, minimum: 0, description: "The income to protect." }
        target_replacement_fraction: { type: number, minimum: 0, maximum: 1, description: "Share of gross income to replace. Omit for 0.65." }
        essential_monthly_expenses: { type: number, minimum: 0, description: "When > 0, overrides the fractional target with a monthly floor." }
        employer_group_fraction: { type: number, minimum: 0, maximum: 1, description: "Fraction of income the group LTD plan replaces (e.g. 0.60)." }
        employer_group_monthly_max: { type: number, minimum: 0, description: "Group benefit cap (0 = uncapped)." }
        employer_paid_premium: { type: boolean, description: "True ⇒ the group benefit is taxable." }
        marginal_tax_rate: { type: number, minimum: 0, maximum: 1, description: "Applied to any taxable benefit." }
        individual_monthly_benefit: { type: number, minimum: 0, description: "Individually-owned policy benefit (tax-free)." }
        ssdi_monthly_benefit: { type: number, minimum: 0, description: "Estimated SSDI monthly benefit (treated as tax-free)." }
        ssdi_offsets_group: { type: boolean, default: true, description: "Whether the group LTD contract reduces its benefit dollar-for-dollar by SSDI (the common 'other income' offset). Default true; set false for a rare non-offsetting plan or when there is no group coverage." }
    DisabilityNeedsResponse:
      type: object
      properties:
        result_id: { type: string, example: disability_1a2b3c4d5e6f7a8b }
        engine_version: { type: string, example: disability-1.0.0 }
        inputs: { $ref: "#/components/schemas/DisabilityNeedsRequest" }
        result:
          type: object
          properties:
            gross_monthly_income: { type: number }
            target_fraction_used: { type: number, description: "Replacement fraction the target reflects — the input fraction, or the fraction essential_monthly_expenses implies when that override is set." }
            monthly_target: { type: number, description: "The income to replace." }
            employer_group_gross: { type: number, description: "Group benefit before tax, net of any SSDI offset." }
            employer_group_ssdi_offset: { type: number, description: "SSDI amount the group plan offset (0 if not offsetting)." }
            employer_group_net: { type: number, description: "After tax if employer-paid." }
            employer_benefit_taxable: { type: boolean }
            individual_net: { type: number }
            ssdi_net: { type: number }
            total_existing_net: { type: number, description: "Net monthly benefits already in place." }
            monthly_gap: { type: number, description: "max(0, target − existing net)." }
            annual_gap: { type: number }
            recommended_additional_monthly: { type: number, description: "= monthly gap." }
            current_replacement_ratio: { type: number, description: "Existing net / gross monthly." }
            coverage_adequate: { type: boolean }
        disclaimer: { type: string }
    LongTermCareRequest:
      type: object
      required: [current_age, care_start_age, annual_care_cost_today, care_duration_years]
      properties:
        current_age: { type: integer, minimum: 0, maximum: 110 }
        care_start_age: { type: integer, minimum: 0, maximum: 110, description: "Expected age care begins (>= current_age)." }
        annual_care_cost_today: { type: number, minimum: 0, description: "Today's annual cost of the expected setting." }
        care_duration_years: { type: integer, minimum: 1, maximum: 40, description: "Expected length of the care episode." }
        care_cost_inflation: { type: number, minimum: -0.5, maximum: 0.5, description: "Annual growth of care costs. Omit for 0.045." }
        discount_rate: { type: number, minimum: -0.5, maximum: 0.5, description: "Present-values future costs to today. Omit for 0.04." }
        annual_premium: { type: number, minimum: 0, description: "Level annual premium in today's dollars (with policy_annual_benefit, enables the comparison); not inflated." }
        premium_years: { type: integer, minimum: 0, maximum: 80, description: "Years premiums are paid (0 ⇒ until care starts)." }
        policy_annual_benefit: { type: number, minimum: 0, description: "Annual benefit at purchase (today); the inflation rider grows it from today, so it has compounded over the years until care starts." }
        policy_benefit_years: { type: integer, minimum: 0, maximum: 40, description: "Benefit duration (0 ⇒ the full care episode)." }
        policy_benefit_inflation: { type: number, minimum: -0.5, maximum: 0.5, description: "Inflation rider on the benefit (0 = level), compounding from the purchase date." }
    LongTermCareResponse:
      type: object
      properties:
        result_id: { type: string, example: ltc_1a2b3c4d5e6f7a8b }
        engine_version: { type: string, example: ltc-1.0.0 }
        inputs: { $ref: "#/components/schemas/LongTermCareRequest" }
        result:
          type: object
          properties:
            years_until_care: { type: integer }
            care_inflation_used: { type: number }
            discount_rate_used: { type: number }
            future_annual_cost_at_start: { type: number, description: "Today's cost inflated to the care-start age." }
            total_care_cost_nominal: { type: number, description: "Sum over the care years (undiscounted)." }
            present_value_of_care: { type: number, description: "Self-insure cost, present-valued to today." }
            nest_egg_needed_at_care_start: { type: number, description: "PV of the care stream at the care-start age." }
            policy:
              type: object
              description: "Present only when a policy is described."
              properties:
                total_premiums_nominal: { type: number }
                present_value_premiums: { type: number }
                present_value_benefits: { type: number, description: "Covered portion of the care stream." }
                present_value_out_of_pocket: { type: number }
                net_cost_with_policy: { type: number, description: "PV premiums + PV out-of-pocket." }
                coverage_ratio: { type: number, description: "PV benefits / PV care." }
                insuring_saves_pv: { type: number, description: "PV(self-insure) − net cost with policy (>0 = policy cheaper on this scenario)." }
        disclaimer: { type: string }
    RebalanceRequest:
      type: object
      required: [holdings]
      properties:
        holdings:
          type: array
          minItems: 1
          maxItems: 200
          items:
            type: object
            required: [value, target_weight]
            properties:
              label: { type: string }
              value: { type: number, minimum: 0, description: "Current market value." }
              target_weight: { type: number, minimum: 0, maximum: 1, description: "Desired share of the portfolio." }
              cost_basis: { type: number, minimum: 0, description: "For the realized-gain estimate (tax-aware). Omit when unknown — the holding is then excluded from the estimate and counted in holdings_missing_basis, not booked as all-gain; set 0 for a genuinely zero-basis lot." }
          description: "The portfolio; target_weight values must sum to 1."
        new_contribution: { type: number, minimum: 0, description: "Cash added before rebalancing." }
        withdrawal: { type: number, minimum: 0, description: "Cash raised before rebalancing." }
        band: { type: number, minimum: 0, maximum: 1, description: "Per-holding drift tolerance; 0 flags any nonzero trade." }
        tax_aware: { type: boolean, description: "Adds the realized-gain estimate and the contributions-first plan." }
    RebalanceResponse:
      type: object
      properties:
        result_id: { type: string, example: rebalance_1a2b3c4d5e6f7a8b }
        engine_version: { type: string, example: rebalance-1.0.0 }
        inputs: { $ref: "#/components/schemas/RebalanceRequest" }
        result:
          type: object
          properties:
            total_current: { type: number, description: "Portfolio value before cash flows." }
            investable_total: { type: number, description: "After contribution − withdrawal (the rebalance base)." }
            trades:
              type: array
              items: { $ref: "#/components/schemas/RebalanceTrade" }
            total_buys: { type: number }
            total_sells: { type: number }
            max_drift: { type: number, description: "Largest absolute drift." }
            needs_rebalance: { type: boolean, description: "Any holding out of band (or any trade if band=0)." }
            estimated_realized_gain: { type: number, description: "From the full rebalance's sells (tax-aware, needs cost_basis)." }
            holdings_missing_basis: { type: integer, description: "Sold holdings excluded from the realized-gain estimate for lack of a cost_basis." }
            contributions_first:
              type: object
              description: "Buy-only plan directing new cash to underweight holdings (tax-aware)."
              properties:
                contribution_allocated: { type: number, description: "Cash actually directed (≤ new_contribution)." }
                max_drift_after: { type: number, description: "Largest absolute drift once the cash is deployed." }
                fully_rebalanced: { type: boolean, description: "True if the contribution alone reached every target." }
        disclaimer: { type: string }
    RebalanceTrade:
      type: object
      description: "The per-holding rebalancing action."
      properties:
        label: { type: string }
        value: { type: number }
        current_weight: { type: number }
        target_weight: { type: number }
        target_value: { type: number }
        drift: { type: number, description: "current_weight − target_weight." }
        trade: { type: number, description: "+ buy, − sell, to reach target." }
        action: { type: string, enum: [buy, sell, hold] }
        out_of_band: { type: boolean }
        contribution: { type: number, description: "Cash directed here in the contributions-first plan." }
    GlidepathRequest:
      type: object
      required: [current_age, end_age]
      properties:
        current_age: { type: integer, minimum: 0, maximum: 110 }
        end_age: { type: integer, minimum: 0, maximum: 110, description: "Project the path through this age (>= current_age)." }
        method: { type: string, enum: [rule_of_thumb, linear], default: rule_of_thumb }
        rule_base: { type: number, minimum: 0, maximum: 200, description: "rule_of_thumb: equity = (rule_base − age)/100. Omit for 110." }
        equity_floor: { type: number, minimum: 0, maximum: 1, description: "rule_of_thumb lower clamp. Omit for 0.20, or set 0 to allow an all-bonds floor." }
        equity_ceiling: { type: number, minimum: 0, maximum: 1, description: "rule_of_thumb upper clamp. Omit for 0.95." }
        retirement_age: { type: integer, minimum: 0, maximum: 110, description: "linear: the glide landing age." }
        start_equity: { type: number, minimum: 0, maximum: 1, description: "linear: equity at current_age." }
        end_equity: { type: number, minimum: 0, maximum: 1, description: "linear: equity at retirement_age." }
        glide_type: { type: string, enum: [to, through], default: to, description: "linear: hold after retirement (to) or keep gliding (through)." }
        final_equity: { type: number, minimum: 0, maximum: 1, description: "through: equity reached by final_age. Omit = end_equity; set 0 to glide to an all-bonds landing." }
        final_age: { type: integer, minimum: 0, maximum: 110, description: "through: age final_equity is reached. Omit = end_age." }
    GlidepathResponse:
      type: object
      properties:
        result_id: { type: string, example: glidepath_1a2b3c4d5e6f7a8b }
        engine_version: { type: string, example: glidepath-1.0.0 }
        inputs: { $ref: "#/components/schemas/GlidepathRequest" }
        result:
          type: object
          properties:
            method: { type: string }
            path:
              type: array
              items: { $ref: "#/components/schemas/GlidepathPoint" }
            current: { $ref: "#/components/schemas/GlidepathPoint" }
            at_end: { $ref: "#/components/schemas/GlidepathPoint" }
            at_retirement: { $ref: "#/components/schemas/GlidepathPoint" }
        disclaimer: { type: string }
    GlidepathPoint:
      type: object
      properties:
        age: { type: integer }
        equity_weight: { type: number }
        bond_weight: { type: number }
    StudentLoanRequest:
      type: object
      required: [loan_balance, interest_rate, agi, family_size]
      properties:
        loan_balance: { type: number, minimum: 0, description: "Current federal loan balance." }
        interest_rate: { type: number, minimum: 0, maximum: 0.3, description: "Annual interest rate (e.g. 0.065)." }
        agi: { type: number, minimum: 0, description: "Borrower's adjusted gross income." }
        family_size: { type: integer, minimum: 1, maximum: 20, description: "Household size (IBR discretionary income)." }
        dependents: { type: integer, minimum: 0, maximum: 20, description: "Number of dependents (RAP −$50/mo each)." }
        state: { type: string, description: "\"AK\"/\"HI\" or empty for the 48 contiguous (poverty-guideline region)." }
        guideline_year: { type: integer, description: "Poverty-guideline year (default 2026)." }
        loan_before_2014: { type: boolean, description: "Pre-2014 loans use 15% / 25-year IBR; 2014+ use 10% / 20-year." }
        standard_term_months: { type: integer, minimum: 0, maximum: 600, description: "Standard-plan term (default 120)." }
        pursuing_pslf: { type: boolean, description: "Add the 120-payment PSLF outcome." }
    StudentLoanResponse:
      type: object
      properties:
        result_id: { type: string, example: studentloan_1a2b3c4d5e6f7a8b }
        engine_version: { type: string, example: student-loan-1.0.0 }
        inputs: { $ref: "#/components/schemas/StudentLoanRequest" }
        result:
          type: object
          properties:
            standard:
              type: object
              properties:
                monthly_payment: { type: number }
                total_paid: { type: number }
                total_interest: { type: number }
                months: { type: integer }
            rap: { $ref: "#/components/schemas/StudentLoanProjection" }
            ibr: { $ref: "#/components/schemas/StudentLoanProjection" }
            ibr_rate: { type: number, description: "0.10 or 0.15." }
            ibr_discretionary_income: { type: number }
            poverty_guideline: { type: number, description: "Annual, for the family size + region." }
            pslf:
              type: object
              description: "Present only when pursuing_pslf is set."
              properties:
                months: { type: integer, description: "120." }
                rap_total_paid: { type: number }
                rap_forgiven: { type: number }
                ibr_total_paid: { type: number }
                ibr_forgiven: { type: number }
            lowest_initial_payment: { type: string, enum: [standard, rap, ibr] }
        disclaimer: { type: string }
    StudentLoanProjection:
      type: object
      description: "An income-driven plan's outcome, holding AGI constant."
      properties:
        initial_monthly_payment: { type: number }
        months_to_payoff: { type: integer, description: "0 if forgiven before payoff." }
        total_paid: { type: number, description: "Borrower payments until payoff or forgiveness." }
        forgiven_balance: { type: number, description: "Balance forgiven at the term (0 if paid off)." }
        forgiven: { type: boolean }
        forgiveness_months: { type: integer, description: "The plan's forgiveness term." }
    StudentLoanRefiRequest:
      type: object
      required: [federal, refi_rate]
      properties:
        federal: { $ref: "#/components/schemas/StudentLoanRequest" }
        refi_rate: { type: number, minimum: 0, maximum: 0.3, description: "Annual fixed rate offered by the private lender." }
        refi_term_months: { type: integer, minimum: 0, maximum: 600, description: "Private loan term; omit for 120." }
        discount_rate: { type: number, minimum: 0, maximum: 0.3, description: "Annual, for present-value scoring; omit for 0.05, or set 0 to compare nominal totals (no discounting)." }
    StudentLoanRefiResponse:
      type: object
      properties:
        result_id: { type: string, example: slrefi_1a2b3c4d5e6f7a8b }
        engine_version: { type: string, example: student-loan-refi-1.0.0 }
        inputs: { $ref: "#/components/schemas/StudentLoanRefiRequest" }
        result:
          type: object
          properties:
            refi:
              type: object
              properties:
                monthly_payment: { type: number }
                total_paid: { type: number }
                total_interest: { type: number }
                months: { type: integer }
                present_value: { type: number, description: "PV of the payment stream at the discount rate." }
            best_federal: { $ref: "#/components/schemas/FederalOption" }
            federal_options:
              type: array
              description: "Every federal candidate considered, scored on the same present-value basis."
              items: { $ref: "#/components/schemas/FederalOption" }
            forgiveness_forfeited: { type: number, description: "Federal balance the best option forgives (lost by refinancing)." }
            recommendation: { type: string, enum: [refinance, stay_federal] }
            present_value_savings: { type: number, description: "best-federal PV − refi PV (positive ⇒ refinancing is cheaper)." }
            breakeven_refi_rate: { type: number, description: "Refi rate whose PV ties the standard federal plan (pure-rate signal)." }
        disclaimer: { type: string }
    FederalOption:
      type: object
      description: "One federal repayment candidate, scored by present value of payments."
      properties:
        plan: { type: string, enum: [standard, rap, ibr, rap_pslf, ibr_pslf] }
        total_paid: { type: number }
        forgiven_balance: { type: number }
        present_value: { type: number }
    RefinanceRequest:
      type: object
      required: [current_balance, current_rate, current_remaining_months, new_rate, new_term_months, closing_costs]
      properties:
        current_balance: { type: number, minimum: 0 }
        current_rate: { type: number, minimum: 0, maximum: 0.3, description: "Annual APR on the existing loan." }
        current_remaining_months: { type: integer, minimum: 1, maximum: 600 }
        new_rate: { type: number, minimum: 0, maximum: 0.3, description: "Annual APR on the refinance." }
        new_term_months: { type: integer, minimum: 1, maximum: 600 }
        closing_costs: { type: number, minimum: 0 }
        roll_in_closing_costs: { type: boolean, description: "Finance the closing costs into the new loan." }
    RefinanceResponse:
      type: object
      properties:
        result_id: { type: string, example: refi_1a2b3c4d5e6f7a8b }
        engine_version: { type: string, example: refinance-1.0.0 }
        inputs: { $ref: "#/components/schemas/RefinanceRequest" }
        result:
          type: object
          properties:
            current_payment: { type: number, description: "Current monthly P&I." }
            new_loan_amount: { type: number, description: "Balance (+ closing costs if rolled in)." }
            new_payment: { type: number, description: "New monthly P&I." }
            monthly_savings: { type: number, description: "current − new." }
            breakeven_months: { type: integer, description: "Months to recoup closing costs (0 if never)." }
            worthwhile: { type: boolean, description: "The new payment is lower." }
            current_total_remaining: { type: number, description: "payment × remaining months." }
            new_total_cost: { type: number, description: "closing costs + new payment × new term." }
            lifetime_savings: { type: number, description: "current total − new total (note: terms may differ)." }
        disclaimer: { type: string }
    PointsRequest:
      type: object
      required: [loan_amount, base_rate, buydown_rate, points]
      properties:
        loan_amount: { type: number, minimum: 0, maximum: 1000000000 }
        term_months: { type: integer, minimum: 0, maximum: 600, description: "Omit for 360." }
        base_rate: { type: number, minimum: 0, maximum: 0.3, description: "Annual APR with no points." }
        buydown_rate: { type: number, minimum: 0, maximum: 0.3, description: "Annual APR after buying points (≤ base_rate)." }
        points: { type: number, minimum: 0, maximum: 20, description: "Number of discount points; each = 1% of the loan." }
    PointsResponse:
      type: object
      properties:
        result_id: { type: string, example: points_1a2b3c4d5e6f7a8b }
        engine_version: { type: string, example: points-1.0.0 }
        inputs: { $ref: "#/components/schemas/PointsRequest" }
        result:
          type: object
          properties:
            points_cost: { type: number, description: "points × 1% × loan." }
            base_payment: { type: number, description: "Monthly P&I at the base rate." }
            buydown_payment: { type: number, description: "Monthly P&I at the bought-down rate." }
            monthly_savings: { type: number, description: "base − buydown." }
            breakeven_months: { type: integer, description: "Months to recoup the points cost (0 if never)." }
            worthwhile: { type: boolean, description: "There is a positive monthly saving." }
            net_savings_full_term: { type: number, description: "saving × term − points cost (if held to term)." }
        disclaimer: { type: string }
    TermComparisonRequest:
      type: object
      required: [loan_amount, short_rate, long_rate, investment_return]
      properties:
        loan_amount: { type: number, minimum: 0, maximum: 1000000000 }
        short_term_months: { type: integer, minimum: 0, maximum: 600, description: "Shorter loan; omit for 180 (15 yr)." }
        short_rate: { type: number, minimum: 0, maximum: 0.3, description: "Annual APR on the shorter loan." }
        long_term_months: { type: integer, minimum: 0, maximum: 600, description: "Longer loan; omit for 360 (30 yr)." }
        long_rate: { type: number, minimum: 0, maximum: 0.3, description: "Annual APR on the longer loan." }
        investment_return: { type: number, minimum: 0, maximum: 0.3, description: "Annual return on invested cash." }
    TermComparisonResponse:
      type: object
      properties:
        result_id: { type: string, example: termcmp_1a2b3c4d5e6f7a8b }
        engine_version: { type: string, example: term-comparison-1.0.0 }
        inputs: { $ref: "#/components/schemas/TermComparisonRequest" }
        result:
          type: object
          properties:
            short_payment: { type: number, description: "Monthly P&I on the shorter loan." }
            long_payment: { type: number, description: "Monthly P&I on the longer loan." }
            monthly_difference: { type: number, description: "short − long." }
            short_total_interest: { type: number, description: "Interest paid over the shorter loan." }
            long_total_interest: { type: number, description: "Interest paid over the longer loan." }
            interest_saved: { type: number, description: "long − short (the short loan's interest saving)." }
            short_path_wealth: { type: number, description: "Pay off fast, then invest the full payment for the rest, valued at the longer horizon." }
            long_path_wealth: { type: number, description: "Keep the long loan and invest the monthly difference throughout, valued at the longer horizon." }
            better_path: { type: string, enum: [short_term, long_term_invest] }
            wealth_advantage: { type: number, description: "|short path − long path| at the longer horizon." }
        disclaimer: { type: string }
    RentVsBuyRequest:
      type: object
      required: [home_price, down_payment, mortgage_rate, monthly_rent, investment_return]
      properties:
        home_price: { type: number, minimum: 0, maximum: 1000000000 }
        down_payment: { type: number, minimum: 0, description: "≤ home_price." }
        mortgage_rate: { type: number, minimum: 0, maximum: 0.3, description: "Annual APR." }
        loan_term_months: { type: integer, minimum: 0, maximum: 600, description: "Omit for 360." }
        closing_cost_rate: { type: number, minimum: 0, maximum: 0.2, description: "Buyer's closing costs, fraction of price; omit for 0.03." }
        selling_cost_rate: { type: number, minimum: 0, maximum: 0.2, description: "Sale costs, fraction of price; omit for 0.06." }
        property_tax_rate: { type: number, minimum: 0, maximum: 0.1, description: "Annual, fraction of home value; omit for 0.011." }
        insurance_rate: { type: number, minimum: 0, maximum: 0.1, description: "Annual homeowner's, fraction of home value; omit for 0.005." }
        maintenance_rate: { type: number, minimum: 0, maximum: 0.1, description: "Annual, fraction of home value; omit for 0.01." }
        hoa_monthly: { type: number, minimum: 0, maximum: 1000000000 }
        home_appreciation: { type: number, minimum: -0.2, maximum: 0.3, description: "Annual; omit for 0.03, or set 0 for a flat market (a bare 0 is honored, not defaulted)." }
        monthly_rent: { type: number, minimum: 0, maximum: 1000000000 }
        rent_growth: { type: number, minimum: -0.2, maximum: 0.3, description: "Annual; omit for 0.03, or set 0 for flat rent (e.g. rent-controlled)." }
        renters_insurance_monthly: { type: number, minimum: 0, maximum: 1000000000 }
        holding_period_months: { type: integer, minimum: 0, maximum: 600, description: "Omit for 84 (7 yr)." }
        investment_return: { type: number, minimum: 0, maximum: 0.3, description: "Annual; the discount rate / opportunity cost." }
        inflation: { type: number, minimum: -0.2, maximum: 0.3, description: "Annual; grows HOA + renters insurance; omit for 0.025, or set 0 to hold them flat." }
    RentVsBuyResponse:
      type: object
      properties:
        result_id: { type: string, example: rentvsbuy_1a2b3c4d5e6f7a8b }
        engine_version: { type: string, example: rent-vs-buy-1.0.0 }
        inputs: { $ref: "#/components/schemas/RentVsBuyRequest" }
        result:
          type: object
          properties:
            monthly_mortgage_payment: { type: number }
            upfront_cost: { type: number, description: "Down payment + buyer's closing costs." }
            home_value_at_sale: { type: number }
            loan_balance_at_sale: { type: number }
            sale_proceeds: { type: number, description: "Home value × (1 − selling cost) − remaining balance." }
            total_rent_paid: { type: number, description: "Nominal sum of rent over the holding period." }
            buy_net_present_cost: { type: number, description: "PV of all ownership flows, net of sale proceeds." }
            rent_net_present_cost: { type: number, description: "PV of all rental flows." }
            cost_difference: { type: number, description: "rent − buy (positive ⇒ buying is cheaper by this PV)." }
            recommendation: { type: string, enum: [buy, rent] }
        disclaimer: { type: string }
    AffordabilityRequest:
      type: object
      required: [annual_income, monthly_debts, down_payment, interest_rate]
      properties:
        annual_income: { type: number, minimum: 0, description: "Gross." }
        monthly_debts: { type: number, minimum: 0, description: "Existing minimum payments (back-end DTI)." }
        down_payment: { type: number, minimum: 0 }
        interest_rate: { type: number, minimum: 0, maximum: 0.3, description: "Annual mortgage APR." }
        term_months: { type: integer, minimum: 0, maximum: 600, description: "Omit for 360." }
        front_end_dti: { type: number, minimum: 0, maximum: 1, description: "Housing / income; omit for 0.28." }
        back_end_dti: { type: number, minimum: 0, maximum: 1, description: "Total debt / income; omit for 0.36." }
        property_tax_rate: { type: number, minimum: 0, maximum: 0.1, description: "Annual; omit for 0.011." }
        insurance_rate: { type: number, minimum: 0, maximum: 0.1, description: "Annual; omit for 0.005." }
        pmi_rate: { type: number, minimum: 0, maximum: 0.1, description: "Annual, on the loan when LTV > 80%; omit for 0.007." }
        hoa_monthly: { type: number, minimum: 0 }
    AffordabilityResponse:
      type: object
      properties:
        result_id: { type: string, example: afford_1a2b3c4d5e6f7a8b }
        engine_version: { type: string, example: affordability-1.0.0 }
        inputs: { $ref: "#/components/schemas/AffordabilityRequest" }
        result:
          type: object
          properties:
            max_housing_payment: { type: number, description: "The binding PITI+HOA budget (monthly)." }
            binding_limit: { type: string, enum: [front_end, back_end] }
            max_loan: { type: number }
            max_home_price: { type: number, description: "max loan + down payment." }
            principal_interest: { type: number }
            property_tax: { type: number }
            insurance: { type: number }
            pmi: { type: number }
            hoa: { type: number }
        disclaimer: { type: string }
    DependentCareRequest:
      type: object
      required: [tax_year, filing_status, ordinary_income, dependent_care_expenses, qualifying_count, marginal_tax_rate]
      properties:
        tax_year: { type: integer, enum: [2025, 2026] }
        filing_status: { type: string, enum: [single, mfj, mfs, hoh] }
        ordinary_income: { type: number, minimum: 0, description: "Drives the §21 applicable percentage (by AGI) + credit headroom." }
        dependent_care_expenses: { type: number, minimum: 0, description: "Total eligible care costs for the year." }
        qualifying_count: { type: integer, minimum: 1, maximum: 10, description: "1 caps §21 at $3k; 2+ at $6k." }
        marginal_tax_rate: { type: number, minimum: 0, maximum: 1, description: "For the FSA saving (income + FICA)." }
        fsa_contribution_limit: { type: number, minimum: 0, description: "Overrides the year's §129 cap (2025: $5,000 / $2,500 MFS; 2026: $7,500 / $3,750, per OBBBA §70404). Set it to model an employer plan that hasn't adopted the increase." }
    DependentCareResponse:
      type: object
      properties:
        result_id: { type: string, example: depcare_1a2b3c4d5e6f7a8b }
        engine_version: { type: string, example: dependent-care-1.0.0 }
        inputs: { $ref: "#/components/schemas/DependentCareRequest" }
        result:
          type: object
          properties:
            fsa_contribution: { type: number, description: "min(expenses, the §129 limit)." }
            fsa_benefit: { type: number, description: "contribution × marginal rate." }
            section_21_credit: { type: number, description: "The §21 credit on the expenses (nonrefundable)." }
            recommended: { type: string, enum: [fsa, credit, either] }
            better_by: { type: number, description: "The benefit gap between the two." }
        disclaimer: { type: string }
    HealthPlanRequest:
      type: object
      required: [expected_medical_spend, marginal_tax_rate, hdhp, ppo]
      properties:
        expected_medical_spend: { type: number, minimum: 0, description: "Expected covered claims for the year." }
        marginal_tax_rate: { type: number, minimum: 0, maximum: 1, description: "For pre-tax HSA/FSA savings (income + FICA)." }
        hdhp: { $ref: "#/components/schemas/HealthPlanDesign" }
        ppo: { $ref: "#/components/schemas/HealthPlanDesign" }
        employer_hsa_seed: { type: number, minimum: 0, description: "Employer's HSA contribution (free money; HDHP)." }
        hsa_contribution: { type: number, minimum: 0, description: "Employee HSA contribution (its tax saving reduces net cost)." }
        ppo_fsa_contribution: { type: number, minimum: 0, description: "A PPO health-FSA contribution (its tax saving reduces net cost)." }
    HealthPlanDesign:
      type: object
      properties:
        premium: { type: number, minimum: 0, description: "Annual employee premium." }
        deductible: { type: number, minimum: 0 }
        oop_max: { type: number, minimum: 0, description: "Out-of-pocket maximum (>= deductible)." }
        coinsurance: { type: number, minimum: 0, maximum: 1, description: "Member share after the deductible." }
    HealthPlanResponse:
      type: object
      properties:
        result_id: { type: string, example: healthplan_1a2b3c4d5e6f7a8b }
        engine_version: { type: string, example: health-plan-1.0.0 }
        inputs: { $ref: "#/components/schemas/HealthPlanRequest" }
        result:
          type: object
          properties:
            hdhp: { $ref: "#/components/schemas/HealthPlanCost" }
            ppo: { $ref: "#/components/schemas/HealthPlanCost" }
            recommended: { type: string, enum: [hdhp, ppo, either] }
            savings: { type: number, description: "Net-cost difference (absolute)." }
            hsa_contribution: { type: number, description: "Echoed — it also builds a tax-advantaged balance beyond this year." }
        disclaimer: { type: string }
    HealthPlanCost:
      type: object
      properties:
        premium: { type: number }
        member_care: { type: number, description: "Cost-sharing paid on the expected spend." }
        tax_savings: { type: number, description: "HSA (HDHP) or FSA (PPO) pre-tax saving." }
        employer_seed: { type: number, description: "HDHP only." }
        net_cost: { type: number, description: "premium + member care − seed − tax savings." }
    FilingStatusRequest:
      type: object
      required: [tax_year, spouse_a, spouse_b]
      properties:
        tax_year: { type: integer, enum: [2025, 2026] }
        spouse_a: { $ref: "#/components/schemas/FilingStatusSpouse" }
        spouse_b: { $ref: "#/components/schemas/FilingStatusSpouse" }
    FilingStatusSpouse:
      type: object
      properties:
        ordinary_income: { type: number, minimum: 0 }
        qualified_income: { type: number, minimum: 0, description: "LTCG + qualified dividends." }
        itemized_deductions: { type: number, minimum: 0, description: "Used if it exceeds the standard deduction." }
        wages: { type: number, minimum: 0, description: "For the Additional Medicare Tax (threshold differs by status)." }
    FilingStatusResponse:
      type: object
      properties:
        result_id: { type: string, example: filingstatus_1a2b3c4d5e6f7a8b }
        engine_version: { type: string, example: filing-status-1.0.0 }
        inputs: { $ref: "#/components/schemas/FilingStatusRequest" }
        result:
          type: object
          properties:
            mfj_tax: { type: number, description: "Total federal tax filing jointly." }
            mfs_tax_total: { type: number, description: "Spouse A + spouse B filing separately." }
            mfs_tax_spouse_a: { type: number }
            mfs_tax_spouse_b: { type: number }
            recommended: { type: string, enum: [mfj, mfs, either] }
            separate_saving: { type: number, description: "MFJ − MFS (positive ⇒ MFS is cheaper)." }
            single_tax_total: { type: number, description: "What the two would owe as single filers." }
            marriage_penalty_or_bonus: { type: number, description: "MFJ − single total (>0 penalty, <0 bonus)." }
            marriage_effect: { type: string, enum: [penalty, bonus, neutral] }
        disclaimer: { type: string }
    RothVsTraditionalRequest:
      type: object
      required: [annual_contribution, years, expected_return, current_marginal_rate, retirement_marginal_rate]
      properties:
        annual_contribution: { type: number, minimum: 0, description: "Dollars into the account each year." }
        years: { type: integer, minimum: 1, maximum: 70, description: "Years until retirement." }
        expected_return: { type: number, minimum: -0.5, maximum: 0.5, description: "Annual, e.g. 0.07." }
        current_marginal_rate: { type: number, minimum: 0, maximum: 1, description: "Tax rate now (drives the traditional tax saving)." }
        retirement_marginal_rate: { type: number, minimum: 0, maximum: 1, description: "Expected rate on traditional withdrawals." }
        invest_tax_savings: { type: boolean, description: "Invest the traditional path's up-front tax saving in a taxable side account (default true — the fair, equal-out-of-pocket comparison)." }
        capital_gains_rate: { type: number, minimum: 0, maximum: 1, description: "Taxes the side account's gains at the end. Omit for 0.15; set it to 0 to model the 0% long-term capital-gains bracket (a low-income retiree)." }
    RothVsTraditionalResponse:
      type: object
      properties:
        result_id: { type: string, example: rothvtrad_1a2b3c4d5e6f7a8b }
        engine_version: { type: string, example: roth-vs-traditional-1.0.0 }
        inputs: { $ref: "#/components/schemas/RothVsTraditionalRequest" }
        result:
          type: object
          properties:
            account_future_value: { type: number, description: "The balance both paths accumulate in the account." }
            roth_after_tax: { type: number, description: "Tax-free." }
            traditional_account_after_tax: { type: number, description: "account × (1 − retirement rate)." }
            taxable_side_after_tax: { type: number, description: "The invested tax saving, net of cap-gains tax." }
            traditional_after_tax: { type: number, description: "Account after-tax + side account." }
            roth_advantage: { type: number, description: "roth − traditional (positive ⇒ Roth wins)." }
            recommended: { type: string, enum: [roth, traditional, either] }
            breakeven_retirement_rate: { type: number, description: "The retirement rate at which the two tie." }
        disclaimer: { type: string }
    DebtPayoffRequest:
      type: object
      required: [debts, monthly_budget]
      properties:
        debts:
          type: array
          minItems: 1
          maxItems: 50
          items:
            type: object
            required: [balance, annual_rate, minimum_payment]
            properties:
              label: { type: string }
              balance: { type: number, minimum: 0 }
              annual_rate: { type: number, minimum: 0, maximum: 0.5, description: "APR, e.g. 0.229." }
              minimum_payment: { type: number, minimum: 0, description: "Required monthly minimum." }
        monthly_budget: { type: number, minimum: 0, description: "Total available per month; must be >= the sum of minimum payments." }
        strategy: { type: string, enum: [avalanche, snowball, both], default: both }
    DebtPayoffResponse:
      type: object
      properties:
        result_id: { type: string, example: debtpayoff_1a2b3c4d5e6f7a8b }
        engine_version: { type: string, example: debt-payoff-1.0.0 }
        inputs: { $ref: "#/components/schemas/DebtPayoffRequest" }
        result:
          type: object
          properties:
            avalanche: { $ref: "#/components/schemas/DebtPayoffPlan" }
            snowball: { $ref: "#/components/schemas/DebtPayoffPlan" }
            interest_saved_by_avalanche: { type: number, description: "Present when both ran: snowball interest − avalanche interest." }
            months_saved_by_avalanche: { type: integer }
        disclaimer: { type: string }
    DebtPayoffPlan:
      type: object
      description: "One strategy's outcome."
      properties:
        strategy: { type: string, enum: [avalanche, snowball] }
        months: { type: integer, description: "Months until all debts clear (600 if not)." }
        total_interest: { type: number }
        total_paid: { type: number }
        paid_off: { type: boolean, description: "All debts cleared within the 50-year horizon." }
        order:
          type: array
          description: "Per debt, in payoff order."
          items:
            type: object
            properties:
              label: { type: string }
              payoff_month: { type: integer, description: "0 if not paid off within the horizon." }
              interest_paid: { type: number }
    SavingsWaterfallRequest:
      type: object
      required: [tax_year, age, filing_status, annual_savings]
      properties:
        tax_year: { type: integer, enum: [2025, 2026] }
        age: { type: integer, minimum: 0, maximum: 100, description: "Drives IRA/401(k)/HSA catch-ups." }
        filing_status: { type: string, enum: [single, mfj, mfs, hoh] }
        magi: { type: number, minimum: 0, description: "For the Roth-IRA phase-out (the IRA-step route)." }
        compensation: { type: number, minimum: 0, description: "Salary / earned income (caps the limits + the match)." }
        annual_savings: { type: number, minimum: 0, description: "The amount to allocate this year." }
        hsa_coverage: { type: string, enum: [none, self, family], description: "HDHP coverage tier (default none)." }
        employer_match_rate: { type: number, minimum: 0, maximum: 5, description: "Match rate (e.g. 0.5 = 50 cents/$)." }
        employer_match_cap_pct: { type: number, minimum: 0, maximum: 1, description: "Match cap as a share of compensation (e.g. 0.06)." }
        covered_by_workplace_plan: { type: boolean, description: "Traditional-IRA deduction phase-out annotation (defaults true when a match/deferral is present)." }
        prior_year_wages: { type: number, minimum: 0, description: "SECURE 2.0 §603 Roth-catch-up flag (2026+)." }
    SavingsWaterfallResponse:
      type: object
      properties:
        result_id: { type: string, example: savingswf_1a2b3c4d5e6f7a8b }
        engine_version: { type: string, example: savings-waterfall-1.0.0 }
        inputs: { $ref: "#/components/schemas/SavingsWaterfallRequest" }
        result:
          type: object
          properties:
            steps:
              type: array
              items:
                type: object
                properties:
                  order: { type: integer }
                  account: { type: string, enum: [401k_match, hsa, ira, 401k_remainder, taxable] }
                  amount: { type: number }
                  limit: { type: number, description: "The account's annual limit (0 = uncapped)." }
                  cumulative: { type: number, description: "Total allocated through this step." }
                  rationale: { type: string }
                  note: { type: string, description: "e.g. the Roth route on the IRA step." }
            total_allocated: { type: number }
            tax_advantaged_allocated: { type: number }
            taxable_allocated: { type: number }
            employer_match_captured: { type: number, description: "The free money secured (prorated if under-funded)." }
            full_match_captured: { type: boolean }
            unused_tax_advantaged_room: { type: number, description: "Headroom left if more were saved." }
            roth_route: { type: string, enum: [roth_direct, roth_partial, backdoor, traditional] }
        disclaimer: { type: string }
    AidEstimateRequest:
      type: object
      required: [family_size, parent_agi, parent_income_tax_paid, parent_earned_income]
      properties:
        award_year: { type: integer, enum: [2026], description: "2026 for the 2026-27 award year (default)." }
        family_size: { type: integer, minimum: 2, maximum: 20, description: "Parents' household size (including the student)." }
        parent_agi: { type: number, minimum: 0 }
        parent_untaxed_income: { type: number, minimum: 0, description: "Untaxed additions (IRA/pension distributions, tax-exempt interest, …)." }
        parent_income_tax_paid: { type: number, minimum: 0, description: "U.S. income tax paid." }
        parent_earned_income: { type: number, minimum: 0, description: "Parent 1 income earned from work." }
        parent2_earned_income: { type: number, minimum: 0, description: "Parent 2 income earned from work (0 if single/non-working)." }
        parent_filing_status: { type: string, enum: [single, mfj, mfs, hoh, qss], default: mfj }
        single_parent: { type: boolean, description: "Affects the employment-expense allowance." }
        parent_cash_savings: { type: number, minimum: 0 }
        parent_investments_net: { type: number, minimum: 0, description: "Net worth of investments (excludes the primary residence)." }
        parent_business_farm_worth: { type: number, minimum: 0, description: "Net worth of businesses/farms (Table A3 adjusts it)." }
        student_agi: { type: number, minimum: 0 }
        student_untaxed_income: { type: number, minimum: 0 }
        student_income_tax_paid: { type: number, minimum: 0 }
        student_earned_income: { type: number, minimum: 0 }
        student_assets: { type: number, minimum: 0 }
        cost_of_attendance: { type: number, minimum: 0, description: "When > 0, yields financial need (COA − SAI)." }
    AidEstimateResponse:
      type: object
      properties:
        result_id: { type: string, example: sai_1a2b3c4d5e6f7a8b }
        engine_version: { type: string, example: sai-1.0.0 }
        inputs: { $ref: "#/components/schemas/AidEstimateRequest" }
        result:
          type: object
          properties:
            parent_available_income: { type: number, description: "PAI." }
            parent_contribution_from_assets: { type: number, description: "PCA = net worth × 12%." }
            parent_adjusted_available_income: { type: number, description: "PAAI = PAI + PCA." }
            parent_contribution: { type: number, description: "Table A5 applied to PAAI." }
            student_contribution_from_income: { type: number }
            student_contribution_from_assets: { type: number }
            income_protection_allowance: { type: number }
            employment_expense_allowance: { type: number }
            sai: { type: integer, description: "The Student Aid Index (floored at −1,500)." }
            financial_need: { type: number, description: "max(0, cost_of_attendance − SAI); 0 if COA not given." }
        disclaimer: { type: string }
    Rollover529Request:
      type: object
      required: [beneficiary_age, balance, account_age_years]
      properties:
        year: { type: integer, description: "For the IRA contribution limit; omit for 2026." }
        beneficiary_age: { type: integer, minimum: 0, maximum: 120, description: "Drives the age-50 IRA catch-up." }
        balance: { type: number, minimum: 0, maximum: 1000000000, description: "Total 529 balance." }
        account_age_years: { type: integer, minimum: 0, maximum: 100, description: "Years the 529 has been open (15+ required)." }
        recent_contributions: { type: number, minimum: 0, maximum: 1000000000, description: "Contributions (+ earnings) from the last 5 years — ineligible to roll." }
        lifetime_rolled_already: { type: number, minimum: 0, maximum: 35000, description: "Prior rollovers counted against the $35k cap." }
        other_ira_contributions: { type: number, minimum: 0, maximum: 1000000000, description: "Beneficiary's other Trad/Roth IRA contributions this year." }
        beneficiary_earned_income: { type: number, minimum: 0, maximum: 1000000000, description: "Compensation; the rollover cannot exceed it." }
    Rollover529Response:
      type: object
      properties:
        result_id: { type: string, example: roll529_1a2b3c4d5e6f7a8b }
        engine_version: { type: string, example: 529-to-roth-1.0.0 }
        inputs: { $ref: "#/components/schemas/Rollover529Request" }
        result:
          type: object
          properties:
            eligible: { type: boolean, description: "The 15-year account-age rule is met." }
            lifetime_cap: { type: number, description: "$35,000." }
            lifetime_remaining: { type: number, description: "Cap − already rolled." }
            annual_ira_limit: { type: number, description: "The year's Roth IRA contribution limit (incl. catch-up)." }
            annual_room: { type: number, description: "IRA limit − other IRA contributions." }
            eligible_balance: { type: number, description: "Balance − recent (last-5-year) contributions." }
            max_rollover_this_year: { type: number, description: "The binding minimum of all the caps." }
            limiting_factor: { type: string, enum: [account_age_under_15_years, annual_ira_limit, lifetime_cap, eligible_balance, earned_income] }
            years_to_rollover_cap: { type: integer, description: "Whole years at this rate to exhaust the lifetime remaining (0 if none possible)." }
        disclaimer: { type: string }
    SelfEmployedRequest:
      type: object
      required: [age, net_profit]
      properties:
        tax_year: { type: integer, description: "Omit for 2026." }
        age: { type: integer, minimum: 0, maximum: 130, description: "Drives the 401(k) catch-up." }
        net_profit: { type: number, minimum: 0, maximum: 1000000000, description: "Schedule C net profit (before the retirement contribution)." }
        existing_w2_deferrals: { type: number, minimum: 0, maximum: 1000000000, description: "Elective deferrals already made this year to another employer's 401(k)/403(b) (per-person §402(g) limit)." }
    SelfEmployedResponse:
      type: object
      properties:
        result_id: { type: string, example: seretire_1a2b3c4d5e6f7a8b }
        engine_version: { type: string, example: self-employed-retirement-1.0.0 }
        inputs: { $ref: "#/components/schemas/SelfEmployedRequest" }
        result:
          type: object
          properties:
            net_earnings_from_se: { type: number, description: "92.35% of net profit." }
            self_employment_tax: { type: number }
            se_tax_deduction: { type: number, description: "½ SE tax." }
            plan_compensation: { type: number, description: "Net profit − ½ SE tax (the base for contributions)." }
            wage_base_used: { type: number, description: "OASDI wage base for the year." }
            annual_additions_limit: { type: number, description: "§415(c)." }
            sep_contribution: { type: number, description: "20% of plan compensation, capped at §415(c)." }
            solo_401k_employee_deferral: { type: number, description: "Elective + catch-up." }
            solo_401k_employer_contribution: { type: number, description: "20% profit-sharing, squeezed under §415(c)." }
            solo_401k_total: { type: number }
            higher_plan: { type: string, enum: [solo_401k, sep, equal] }
        disclaimer: { type: string }
    W4CheckupRequest:
      type: object
      required: [tax, ytd_withheld, per_paycheck_withholding, pay_periods_remaining]
      properties:
        tax:
          allOf:
            - $ref: "#/components/schemas/TaxEstimateRequest"
          description: "The projected federal tax situation for the year (see /v1/tax/estimate)."
        ytd_withheld: { type: number, minimum: 0, maximum: 1000000000000, description: "Federal income tax withheld so far this year." }
        per_paycheck_withholding: { type: number, minimum: 0, maximum: 1000000000000, description: "Federal withholding per remaining paycheck." }
        pay_periods_remaining: { type: integer, minimum: 0, maximum: 53, description: "Paychecks left in the year." }
        prior_year_tax: { type: number, minimum: 0, maximum: 1000000000000, description: "Prior year's total tax (enables the §6654 safe-harbor check)." }
        prior_year_agi: { type: number, minimum: 0, maximum: 1000000000000, description: "Prior year's AGI (picks the 100% vs 110% safe-harbor threshold)." }
    W4CheckupResponse:
      type: object
      properties:
        result_id: { type: string, example: w4_1a2b3c4d5e6f7a8b }
        engine_version: { type: string, example: w4-checkup-1.0.0 }
        inputs: { $ref: "#/components/schemas/W4CheckupRequest" }
        result:
          type: object
          properties:
            projected_federal_tax: { type: number, description: "Year's federal liability (excl. state)." }
            projected_withholding: { type: number, description: "YTD + per-paycheck × remaining." }
            refund_or_balance_due: { type: number, description: "Withholding − tax (positive = refund, negative = owe)." }
            status: { type: string, enum: [on_track, under_withheld, over_withheld] }
            recommended_per_paycheck_change: { type: number, description: "Add to W-4 line 4c if positive; reduce if negative." }
            safe_harbor_checked: { type: boolean }
            safe_harbor_target: { type: number, description: "Min total withholding to avoid a §6654 penalty (when checked)." }
            penalty_risk: { type: boolean, description: "Projected withholding is below the safe harbor." }
        disclaimer: { type: string }
    BatchRequest:
      type: object
      required: [requests]
      properties:
        requests:
          type: array
          minItems: 1
          maxItems: 20
          description: "The sub-requests to run, in order."
          items:
            type: object
            required: [path, body]
            properties:
              path: { type: string, description: "A calc route path, e.g. /v1/tvm (no /v1/batch nesting)." }
              body: { type: object, description: "The sub-request body, exactly as the endpoint expects." }
    BatchResponse:
      type: object
      properties:
        result_id: { type: string, example: batch_1a2b3c4d5e6f7a8b }
        engine_version: { type: string, example: batch-1.0.0 }
        result:
          type: object
          properties:
            results:
              type: array
              description: "One entry per sub-request, order preserved."
              items:
                type: object
                properties:
                  status: { type: integer, description: "The sub-request's HTTP status (200, 400, 503, …)." }
                  body: { type: object, description: "The sub-request's response body (a result envelope, or an error)." }
        disclaimer: { type: string }
    ACAOptimizeRequest:
      type: object
      required: [tax_year, filing_status, age, household_size, benchmark_annual_premium, spending_need]
      properties:
        tax_year: { type: integer, enum: [2025, 2026], description: "ACA-supported years (2026 reintroduces the 400%-FPL cliff)." }
        filing_status: { type: string, enum: [single, mfj, mfs, hoh] }
        age: { type: integer, minimum: 0, maximum: 64, description: "Under 65 (the ACA marketplace is the pre-Medicare context)." }
        household_size: { type: integer, minimum: 1 }
        state: { type: string, description: "FPL region: \"AK\" / \"HI\", or empty for the 48 contiguous states + DC." }
        benchmark_annual_premium: { type: number, minimum: 0, description: "Second-lowest-cost Silver plan, annual." }
        spending_need: { type: number, minimum: 0, description: "Gross withdrawal to fund from the accounts." }
        taxable_balance: { type: number, minimum: 0 }
        taxable_gain_fraction: { type: number, minimum: 0, maximum: 1, description: "Share of a taxable withdrawal that is realized LTCG." }
        deferred_balance: { type: number, minimum: 0, description: "Traditional IRA/401(k) — withdrawals are ordinary income." }
        roth_balance: { type: number, minimum: 0, description: "Roth — withdrawals add no MAGI." }
        base_income: { type: number, minimum: 0, description: "Other ordinary income already in MAGI (pension, interest)." }
        tax_exempt_interest: { type: number, minimum: 0, description: "Added to MAGI for the ACA." }
    ACAOptimizeResponse:
      type: object
      properties:
        result_id: { type: string, example: acaopt_1a2b3c4d5e6f7a8b }
        engine_version: { type: string, example: aca-optimize-1.0.0 }
        inputs: { $ref: "#/components/schemas/ACAOptimizeRequest" }
        result:
          type: object
          properties:
            optimized: { $ref: "#/components/schemas/ACADrawPlan" }
            naive: { $ref: "#/components/schemas/ACADrawPlan" }
            savings: { type: number, description: "Naive total cost minus optimized total cost." }
            cliff_magi: { type: number, description: "The 400%-of-FPL MAGI threshold." }
            cliff_headroom: { type: number, description: "cliff_magi minus the optimized MAGI (negative = unavoidably over)." }
            shortfall: { type: number, description: "spending_need not coverable from the balances (0 if funded)." }
        disclaimer: { type: string }
    ACADrawPlan:
      type: object
      description: "One withdrawal plan and its resulting tax + premium picture (annual dollars)."
      properties:
        draw_roth: { type: number }
        draw_taxable: { type: number }
        draw_deferred: { type: number }
        magi: { type: number }
        fpl_percent: { type: number }
        premium_tax_credit: { type: number }
        net_premium: { type: number, description: "Benchmark minus PTC." }
        income_tax: { type: number, description: "Federal income tax + NIIT on the draws." }
        total_cost: { type: number, description: "income_tax + net_premium." }
        above_cliff: { type: boolean, description: "At/above 400% FPL with no subsidy (2026+)." }
    LiquidationScheduleRequest:
      type: object
      required: [tax_year, filing_status, market_value, cost_basis, objective, horizon_years]
      properties:
        tax_year: { type: integer, enum: [2025, 2026] }
        filing_status: { type: string, enum: [single, mfj, mfs, hoh] }
        market_value: { type: number, minimum: 0, description: "Current value of the concentrated position." }
        cost_basis: { type: number, minimum: 0, description: "Total cost basis (must be <= market_value; gain = market − basis)." }
        other_ordinary_income: { type: number, minimum: 0, description: "AGI-level ordinary income (drives the LTCG stacking position)." }
        other_ltcg: { type: number, minimum: 0, description: "Other long-term gains already realized (stack below this position)." }
        objective: { type: string, enum: [target_bracket, niit_cap, max_annual_gain], description: "Yearly gain target: fill the 15% LTCG bracket, stay under the NIIT threshold, or a fixed cap." }
        max_annual_gain: { type: number, minimum: 0, description: "Required when objective = max_annual_gain." }
        growth_rate: { type: number, minimum: -1, maximum: 1, description: "Annual appreciation of the remaining position." }
        horizon_years: { type: integer, minimum: 1, maximum: 40 }
        state_tax_rate: { type: number, minimum: 0, maximum: 1, description: "Optional flat state rate on realized gains." }
    LiquidationScheduleResponse:
      type: object
      properties:
        result_id: { type: string, example: liquidation_1a2b3c4d5e6f7a8b }
        engine_version: { type: string, example: liquidation-1.0.0 }
        inputs: { $ref: "#/components/schemas/LiquidationScheduleRequest" }
        result:
          type: object
          properties:
            schedule:
              type: array
              description: "One entry per year until the position is sold or the horizon ends."
              items:
                type: object
                properties:
                  year: { type: integer }
                  start_value: { type: number }
                  sold: { type: number, description: "Market value sold this year." }
                  gain: { type: number, description: "Long-term gain realized." }
                  ltcg_tax: { type: number, description: "0/15/20 federal tax on the gain." }
                  niit: { type: number, description: "§1411 NIIT on the gain." }
                  state_tax: { type: number }
                  total_tax: { type: number }
                  after_tax_proceeds: { type: number }
                  remaining_value: { type: number }
            years_to_diversify: { type: integer, description: "Year the position is fully sold (0 if not within the horizon)." }
            fully_liquidated: { type: boolean }
            total_gain_realized: { type: number }
            total_tax: { type: number }
            total_after_tax_proceeds: { type: number }
            lump_sum:
              type: object
              description: "The alternative — sell everything now."
              properties:
                gain: { type: number }
                ltcg_tax: { type: number }
                niit: { type: number }
                state_tax: { type: number }
                total_tax: { type: number }
                after_tax_proceeds: { type: number }
                tax_saved_by_spreading: { type: number, description: "Lump-sum total tax minus the schedule's total tax." }
        disclaimer: { type: string }
    QSBSExclusionRequest:
      type: object
      required: [acquisition_date, holding_period_years, basis, gain, gross_assets_at_issuance, filing_status, tax_year]
      properties:
        acquisition_date: { type: string, description: "YYYY-MM-DD; drives the vintage / applicable-date test.", example: "2015-06-01" }
        holding_period_years: { type: number, minimum: 0, maximum: 100, description: "Years the stock was held." }
        basis: { type: number, minimum: 0, description: "Cost basis (10× basis is part of the per-issuer cap)." }
        gain: { type: number, minimum: 0, description: "Total long-term gain on the sale." }
        gross_assets_at_issuance: { type: number, minimum: 0, description: "Aggregate gross assets at issuance (≤$50M legacy / $75M OBBBA to qualify)." }
        filing_status: { type: string, enum: [single, mfj, mfs, hoh] }
        tax_year: { type: integer, enum: [2025, 2026] }
        other_income: { type: number, minimum: 0, description: "Other ordinary income (positions the excess LTCG + NIIT)." }
    QSBSExclusionResponse:
      type: object
      properties:
        result_id: { type: string, example: qsbs_1a2b3c4d5e6f7a8b }
        engine_version: { type: string, example: qsbs-1.0.0 }
        inputs: { $ref: "#/components/schemas/QSBSExclusionRequest" }
        result:
          type: object
          properties:
            eligible: { type: boolean, description: "False if gross assets exceed the limit or the holding period is too short." }
            exclusion_percent: { type: number, description: "0, 0.50, 0.75, or 1.00." }
            per_issuer_cap: { type: number, description: "Greater of the dollar cap or 10× basis." }
            eligible_gain: { type: number, description: "Gain within the cap." }
            excluded_gain: { type: number, description: "eligible_gain × exclusion_percent (out of income)." }
            section_1202_gain: { type: number, description: "Included eligible gain, taxed as 28%-rate gain." }
            excess_gain: { type: number, description: "Gain beyond the cap, taxed as a regular LTCG." }
            section_1202_tax: { type: number, description: "28%-rate tax on section_1202_gain (capped at incremental ordinary tax)." }
            excess_ltcg_tax: { type: number, description: "0/15/20 tax on the excess gain." }
            niit: { type: number, description: "§1411 NIIT on the taxable gain." }
            total_tax: { type: number }
            amt_preference: { type: number, description: "§57(a)(7): 7% of excluded gain for 50%/75% exclusions." }
            effective_rate: { type: number, description: "total_tax / gain." }
            tax_without_qsbs: { type: number, description: "The whole gain at regular LTCG + NIIT." }
            savings_versus_no_qsbs: { type: number, description: "tax_without_qsbs − total_tax (can be slightly negative for a low-income partial-exclusion seller)." }
        disclaimer: { type: string }
    NUARequest:
      type: object
      required: [filing_status, tax_year, cost_basis, market_value, marginal_ordinary_rate]
      properties:
        filing_status: { type: string, enum: [single, mfj, mfs, hoh] }
        tax_year: { type: integer, enum: [2025, 2026] }
        cost_basis: { type: number, minimum: 0, description: "Plan's basis in the employer stock (ordinary income now under NUA)." }
        market_value: { type: number, minimum: 0, description: "Fair market value at distribution (must be ≥ cost_basis)." }
        marginal_ordinary_rate: { type: number, minimum: 0, maximum: 1, description: "Rate on ordinary income (the basis, and the rollover)." }
        ltcg_rate: { type: number, minimum: 0, maximum: 1, description: "Rate on the appreciation; omit to derive from the ordinary rate." }
        sale_price: { type: number, minimum: 0, description: "FMV at sale; omit to grow market_value by growth_rate." }
        years_to_sale: { type: integer, minimum: 0, maximum: 60, description: "Years the stock is held after distribution." }
        growth_rate: { type: number, minimum: -1, maximum: 1, description: "Annual appreciation to the sale (when sale_price is omitted)." }
        discount_rate: { type: number, minimum: -1, maximum: 1, description: "Real rate for the present value of deferred tax." }
    NUAResponse:
      type: object
      properties:
        result_id: { type: string, example: nua_1a2b3c4d5e6f7a8b }
        engine_version: { type: string, example: nua-1.0.0 }
        inputs: { $ref: "#/components/schemas/NUARequest" }
        result:
          type: object
          properties:
            nua_amount: { type: number, description: "market_value − cost_basis." }
            ltcg_rate_used: { type: number }
            sale_value: { type: number, description: "FMV at sale used in the comparison." }
            nua_basis_tax_now: { type: number, description: "Ordinary tax on the basis at distribution." }
            nua_sale_tax: { type: number, description: "LTCG on the NUA + post-distribution appreciation." }
            nua_total_tax: { type: number }
            nua_total_tax_pv: { type: number }
            rollover_tax: { type: number, description: "Ordinary tax on the whole grown value (nominal)." }
            rollover_tax_pv: { type: number }
            savings_pv: { type: number, description: "rollover PV tax − NUA PV tax (positive ⇒ NUA wins)." }
            recommendation: { type: string, enum: [nua, rollover] }
            breakeven_years: { type: integer, description: "Years held at which the rollover overtakes NUA (−1 = NUA wins through 60)." }
        disclaimer: { type: string }
    SCorpSalaryRequest:
      type: object
      required: [filing_status, tax_year, business_net_income, reasonable_comp_floor]
      properties:
        filing_status: { type: string, enum: [single, mfj, mfs, hoh] }
        tax_year: { type: integer, enum: [2025, 2026] }
        business_net_income: { type: number, minimum: 0, description: "Pass-through profit before owner comp + employer payroll tax." }
        reasonable_comp_floor: { type: number, minimum: 0, description: "Minimum defensible W-2 salary (IRS reasonable comp)." }
        other_income: { type: number, minimum: 0, description: "Owner's other ordinary income (positions brackets + the §199A threshold)." }
        ubia: { type: number, minimum: 0, description: "Unadjusted basis of qualified property (§199A alt limit)." }
        sstb: { type: boolean, description: "Specified service trade or business (deduction phases out above the range)." }
        state_tax_rate: { type: number, minimum: 0, maximum: 1, description: "Optional flat state rate on ordinary income." }
        max_salary: { type: number, minimum: 0, description: "Optional salary cap for the search (default = business_net_income)." }
    SCorpSalaryResponse:
      type: object
      properties:
        result_id: { type: string, example: scorp_1a2b3c4d5e6f7a8b }
        engine_version: { type: string, example: scorp-salary-1.0.0 }
        inputs: { $ref: "#/components/schemas/SCorpSalaryRequest" }
        result:
          type: object
          properties:
            recommended_salary: { type: number, description: "The after-tax-maximizing W-2 salary." }
            distribution: { type: number, description: "Pass-through paid out = net income − salary − employer payroll tax." }
            qbi_deduction: { type: number, description: "§199A deduction at the recommended salary." }
            employer_payroll_tax: { type: number, description: "Deductible; OASDI to the wage base + 1.45% Medicare." }
            employee_payroll_tax: { type: number, description: "OASDI to the wage base + 1.45% + 0.9% Additional Medicare." }
            total_payroll_tax: { type: number }
            income_tax: { type: number, description: "Federal income tax (QBI-aware)." }
            state_tax: { type: number, description: "Flat state rate × ordinary income (0 if unset)." }
            total_tax: { type: number, description: "Payroll + income + state." }
            after_tax_income: { type: number, description: "Net income − total tax." }
            all_wages_after_tax: { type: number, description: "Baseline: the whole net income paid out as W-2 wages." }
            all_wages_total_tax: { type: number }
            savings_vs_all_wages: { type: number, description: "Recommended after-tax − all-wages after-tax." }
            effective_tax_rate: { type: number, description: "total_tax / business_net_income." }
            wage_base_used: { type: number, description: "The year's OASDI wage base." }
        disclaimer: { type: string }
    BackdoorRothRequest:
      type: object
      required: [filing_status, tax_year, age, pretax_ira_balance, nondeductible_contribution]
      properties:
        filing_status: { type: string, enum: [single, mfj, mfs, hoh] }
        tax_year: { type: integer, enum: [2025, 2026] }
        age: { type: integer, minimum: 0, maximum: 120, description: "Drives the IRA contribution limit (age-50 catch-up)." }
        pretax_ira_balance: { type: number, minimum: 0, description: "Aggregate pre-tax across all traditional/SEP/SIMPLE IRAs (deductible + earnings)." }
        existing_basis: { type: number, minimum: 0, description: "Prior after-tax basis (Form 8606 line 14)." }
        nondeductible_contribution: { type: number, minimum: 0, description: "New nondeductible contribution this year." }
        conversion_amount: { type: number, minimum: 0, description: "Amount converted to Roth (0 ⇒ the new contribution; capped at the total IRA value)." }
        elective_deferrals: { type: number, minimum: 0, description: "Employee 401(k) deferrals (pre-tax + Roth), excluding catch-up." }
        employer_contributions: { type: number, minimum: 0, description: "Employer match + profit-sharing." }
        plan_after_tax_limit: { type: number, minimum: 0, description: "Optional plan cap on after-tax contributions (0 = none)." }
    BackdoorRothResponse:
      type: object
      properties:
        result_id: { type: string, example: backdoor_1a2b3c4d5e6f7a8b }
        engine_version: { type: string, example: backdoor-roth-1.0.0 }
        inputs: { $ref: "#/components/schemas/BackdoorRothRequest" }
        result:
          type: object
          properties:
            ira_contribution_limit: { type: number, description: "§219 limit incl. the age-50 catch-up." }
            contribution_within_limit: { type: boolean }
            total_basis: { type: number, description: "Existing basis + new nondeductible contribution." }
            total_ira_value: { type: number, description: "Pre-tax + total basis (the pro-rata denominator)." }
            converted_amount: { type: number, description: "Amount actually converted to Roth." }
            nontaxable_conversion: { type: number, description: "Return of basis (tax-free)." }
            taxable_conversion: { type: number, description: "Pre-tax portion pulled out pro-rata (ordinary income)." }
            taxable_percent: { type: number, description: "taxable / converted." }
            remaining_basis: { type: number, description: "Basis left in the traditional IRA after the conversion." }
            remaining_pretax: { type: number, description: "Pre-tax left after the conversion." }
            is_clean_backdoor: { type: boolean, description: "True when there is no pre-tax balance (conversion fully tax-free)." }
            dc_415c_limit: { type: number, description: "The year's §415(c) annual-additions limit (excl. catch-up)." }
            mega_backdoor_room: { type: number, description: "After-tax room = §415(c) − deferrals − employer (− plan cap)." }
            total_roth_created: { type: number, description: "Converted amount + mega-backdoor room (if both funded)." }
        disclaimer: { type: string }
    QLACRequest:
      type: object
      required: [tax_year, age, account_balance, qlac_premium, start_age, sex]
      properties:
        tax_year: { type: integer, enum: [2025, 2026] }
        age: { type: integer, minimum: 0, maximum: 119, description: "Current age of the IRA owner." }
        birth_year: { type: integer, description: "Sets the SECURE 2.0 RMD beginning age (omit ⇒ 73)." }
        account_balance: { type: number, minimum: 0, description: "Traditional-IRA balance before the QLAC purchase." }
        qlac_premium: { type: number, minimum: 0, description: "Premium paid for the QLAC (capped at the year's limit; ≤ account_balance)." }
        start_age: { type: integer, description: "Age payments begin (≥ age and ≤ 85)." }
        sex: { type: string, enum: [male, female], description: "Mortality pricing." }
        assumed_rate: { type: number, minimum: -1, maximum: 1, description: "Pricing interest rate (omit ⇒ 4.5%)." }
    QLACResponse:
      type: object
      properties:
        result_id: { type: string, example: qlac_1a2b3c4d5e6f7a8b }
        engine_version: { type: string, example: qlac-1.0.0 }
        inputs: { $ref: "#/components/schemas/QLACRequest" }
        result:
          type: object
          properties:
            premium_limit: { type: number, description: "The year's §1.401(a)(9)-6 premium cap." }
            eligible_premium: { type: number, description: "min(premium, limit) — the amount excluded from RMDs." }
            excess_premium: { type: number, description: "Premium over the limit (not QLAC-eligible)." }
            within_limit: { type: boolean }
            rmd_beginning_age: { type: integer, description: "SECURE 2.0 RMD beginning age." }
            rmd_age_used: { type: integer, description: "Age at which the RMD comparison is drawn." }
            rmd_without_qlac: { type: number, description: "RMD on the full balance." }
            rmd_with_qlac: { type: number, description: "RMD on the balance less the eligible premium." }
            annual_rmd_reduction: { type: number, description: "The first-year RMD relief the QLAC buys." }
            income_start_age: { type: integer }
            deferred_annual_income: { type: number, description: "Guaranteed income from the start age." }
            deferred_monthly_income: { type: number }
            expected_lifetime_payout: { type: number, description: "Expected total nominal income over life." }
            moneys_worth: { type: number, description: "PV(income) / premium at the assumed rate." }
            assumed_rate: { type: number, description: "Rate used for pricing." }
        disclaimer: { type: string }
    Error:
      type: object
      properties:
        error:
          type: object
          properties:
            code: { type: string, example: missing_field }
            message: { type: string, example: current_age is required }
