# Solar Payback: public inputs and reproduction recipe

This is an illustrative annual cash-flow model, not an address-specific quote, investment recommendation, roof simulation or promise of savings. Figures labelled assumptions are not market observations.

## Downloads and version identity

- /data/solar-inputs.json: 51 state/DC rows, units, source period, model inputs, benchmark outputs and scoped incentive records.
- /data/solar-inputs.csv: the same 51 rows in a spreadsheet-friendly flat format. Blank payback cells mean no break-even within 25 years, not zero years.
- /data/solar-model.js: readable, self-contained JavaScript generated from the actual calculator; exports calcPayback, estimateAnnualKwh and benchmarkAssumptions.
- /data/state-insolation-pvwatts.json: the per-state NLR PVWatts v8 requests and responses behind the sunlight inputs.

SHA-256 of the public downloads (compare with a hash of the file you downloaded):
- /data/solar-inputs.json: 138e34bf48d6d9561ef1604eff6e36218dde71342835bf8a7b04f19f39b5b895
- /data/solar-inputs.csv: 0ec71829dd449717758d48aa33c8ad69f472544f00a969e626678482981beb58
- /data/solar-model.js: 5483f72407d286f1880e3b3919726dc1ad17c8de354773ed46f57eb5d86b6e0d
- /data/state-insolation-pvwatts.json: c5191d8917f968ae16999019ddb74c3bfa4a40eb50de11f821017577442139b8

SHA-256 of the two source files these downloads are generated from (not published separately; they identify the exact version):
- lib/data/states.json (source data): f6a1c2e9c041c24eac7582872d61158cf492939314bf31010ea8a355bbffe556
- lib/calc.ts (calculator TypeScript): 3eb7c9710dc2bfa87651eab159c846e98cc2a42302f900d548027a1c63182bbe

## Observations versus assumptions

Electricity prices: Calendar year 2024 annual average; residential cents/kWh from https://www.eia.gov/electricity/annual/html/epa_02_10.html. Original retrieval 2026-08-23; all 51 rows rechecked 2026-09-24. Numeric rates are unchanged. They are statewide historical averages, not current utility tariffs.

Sunlight: NSRDB typical-meteorological-year solar data via the NLR (formerly NREL) PVWatts v8 API, one request per state at the U.S. Census Bureau 2020 state center of population, for a 1 kW south-facing (azimuth 180°) fixed roof-mounted array tilted 20°. The value is PVWatts' solrad_annual: average daily solar energy on that tilted panel, in kWh/m²/day. NSRDB PSM V3 GOES tmy-2020 for 49 states and DC; NSRDB PSM V4 Polar tmy-2023 for Alaska (the dataset PVWatts selected for that location). Retrieved 2026-09-24.

Exact sunlight recipe: For each state: take the latitude/longitude of the 2020 Census center of population (CenPop2020_Mean_ST.txt); request PVWatts v8 with system_capacity=1, module_type=0, losses=14, array_type=1, tilt=20, azimuth=180, dataset=nsrdb; use outputs.solrad_annual rounded to two decimals. One location stands in for a whole state, so homes far from that point (and any shaded, east/west or steep roof) will differ. Anyone with a free NLR developer key can repeat the 51 requests; every request and response is published. Per-state requests, weather-file IDs and responses: state-insolation-pvwatts.json. Cross-check: PVWatts' own AC output for the same 1 kW array, divided by this site's shortcut (sunlight × 365 × 0.78), ranges 0.915–1.057 across the 51 locations (median 0.980): the shortcut is within about 2% of PVWatts at the median, about 9% high in the least favourable state (Mississippi, 0.915) and about 5% low at the other extreme (Alaska, 1.057). Values used before 2026-09-24 came from an unsourced scaffold and were replaced. Replace annual generation with a site-specific estimate before evaluating a quote.

Benchmark assumptions: 7 kW DC; 3.2 USD/W installed; performance ratio 0.78; annual production degradation 0.5%; annual import/export price escalation 3%; no automatic federal/state/utility benefits; no recurring costs. Production sensitivity at minus/plus 20% is an illustrative stress test, NOT an uncertainty bound. Flat-price outputs turn off escalation but retain degradation.

Incentives: 17 rows have narrowly verified primary-source statements; 34 explicitly say unknown. Unknown is not none. reviewed_on records the inventory/removal review; verified_on exists only where source claims were checked. Source access dates are not program effective dates. Null effective dates mean not established. Verify territory, applicant eligibility, legacy status, funding and payment timing with the named administrator. No listed benefit enters the benchmark automatically.

## Formula and editable contract

Let E1 be first-year annual production in kWh, d degradation as a fraction, g price escalation as a fraction, f self-consumption as a fraction, r import price in USD/kWh, x export price in USD/kWh, F annual fixed costs, A recurring incentive, N incentive years, and T[y] scheduled receipts at the END of year y.

production[y] = E1 * (1-d)^(y-1)
energy_value[y] = production[y] * (f*r + (1-f)*x) * (1+g)^(y-1)
operating_cash[y] = energy_value[y] - F
end_year_receipts[y] = (y <= N ? A : 0) + T[y]
annual_net_cash[y] = operating_cash[y] + end_year_receipts[y]

Gross cost uses installed_cost_usd when supplied; otherwise system_size_kw * 1000 * cost_per_watt_usd. Initial outlay = max(0, gross - state_credit_usd - utility_rebate_usd). The latter two are legacy UPFRONT fields, not claims that a tax credit arrives immediately. Put a delayed tax benefit in incentive_cashflows instead; never enter the same benefit in both places. Each scheduled {year, amount_usd} is added once at that year-end. Recurring incentives are also received at the end of each eligible year. A crossing made possible by either type of incentive is recognized at year-end, never fractionally before receipt. Only operating cash is spread evenly within a year for simple interpolation. Initial outlay of zero has payback zero; no crossing within 25 years returns null. A first crossing is not a guarantee of permanent cumulative profitability if later costs are negative cash flows.

Optional inputs: installed_cost_usd; export_rate_cents_per_kwh (default import price); self_consumption_pct (default 100); annual_fixed_cost_usd (default 0); annual_incentive_usd (default 0); incentive_years (default 0); incentive_cashflows (default []). Rates are entered in cents/kWh and divided by 100. Degradation/escalation/self-consumption are entered as percentages. Scheduled year is a positive whole number; receipts after year 25 do not enter this model horizon.

Generation shortcut: system_size_kw * insolation_kwh_per_m2_per_day (on a 20° south-facing panel) * 365 * performance_ratio. It does not model your roof's own tilt, azimuth or shading, or simulate storage or dispatch. At a fixed cost/W, fixed production/kW and no fixed-dollar adjustments, increasing kW scales cost and savings together; payback cancels, not personalizes.

## Run it publicly (no private repository needed)

Download solar-model.js and solar-inputs.json. Save the JS as solar-model.mjs for Node, or use it as an ES module in a browser. In Node run:

    import { readFileSync } from 'node:fs';
    import { calcPayback } from './solar-model.mjs';
    const inputs = JSON.parse(readFileSync('./solar-inputs.json', 'utf8'));
    const row = inputs.rows.find(r => r.code === 'CA');
    console.log(calcPayback(row.model_input));
    console.log(calcPayback({ ...row.model_input,
      installed_cost_usd: 20000, estimated_annual_kwh: 10000,
      electricity_rate_cents_per_kwh: 20,
      export_rate_cents_per_kwh: 5, self_consumption_pct: 40,
      annual_rate_inflation_pct: 0, annual_degradation_pct: 0
    }));

The second case is an invented illustration: 4,000 self-consumed kWh at 0.20 USD plus 6,000 exported kWh at 0.05 USD gives 1,100 USD/year, versus 2,000 USD/year if all 10,000 kWh are valued at retail. Its cost and tariff are assumptions, not an observed household or current rate offer. Keep a no-incentive baseline and test cost, production, tariff, fixed costs and payment timing separately.

## Limits

No automatic loan interest, discounting, inflation-adjusted purchasing power, tax liability, carryforward, time-of-use dispatch, credit rollover/expiry, battery degradation/replacement or resale value is modelled. Enter installed storage cost in the cash price if relevant, but that does not simulate battery operation. A constant annual cost cannot reproduce a lumpy replacement payment. The assumed annual blended export value must be adjusted externally for a real tariff's credit limits. A positive result is not financial advice or verified revenue.
