Method

How every number on this site is produced

Nothing here is estimated, smoothed toward a nicer answer, or filled in when a source is unavailable. Where a figure cannot be obtained honestly, the site says so instead of inventing one. This page is the full accounting, including the parts that do not work.

The question this answers

Most cost-of-living tools answer a relative question: given a salary, where does it stretch furthest? That produces a ranking, but a ranking never tells you whether any option on it is actually affordable. Two places with an identical 28% rent burden are not equally livable if one has materially higher transport and medical costs, and a rent-share chart cannot see that difference at all.

So the model here is absolute. For every county it asks what a single adult must spend to live there, then works backwards through the tax code to the salary that covers it. The answer is a dollar figure you can hold an offer up against.

Data sources

Rent: Zillow Observed Rent Index

WhatSmoothed asking rent, all home types (single-family, condo, multifamily)
GeographyCounty and metro
RefreshMonthly, automatically, by a scheduled GitHub Action
CoverageAbout 1,360 of the ~3,140 US counties

ZORI is not a one-bedroom median, and is never labelled as one. Zillow publishes no bedroom-level series at this geography. It is an asking-rent index, so it tracks what is being listed now rather than what existing tenants pay, which makes it lead the market slightly.

The git history of data/county-rents.json is the freshness record: every refresh is a dated, diffable commit, so the claim that this data is current is auditable rather than asserted.

Rent, complete coverage: Census ACS

WhatMedian gross rent (contract rent plus utilities), table B25031, broken out by bedroom count
GeographyCounty
RefreshAnnual, five-year rolling estimate
Coverage3,123 of 3,142 counties (99.4%); 2,920 also publish a one-bedroom figure

Why unit size matters, and why one bedroom is the default

The whole model prices one adult with no children, the same household MIT budgets for. But an all-bedroom median is pulled upward by family-sized units, so using it prices a single person against housing they would not rent. King County is $2,035 across all sizes against $1,813 for a one-bedroom; Los Angeles is $1,893 against $1,661. That is a 10–17% overstatement flowing straight into every salary threshold, which is why the default is a one-bedroom rather than the headline median.

Studio and two-bedroom figures are available too. Rarer sizes are suppressed in thin counties, so each falls back to the overall median and says which figure it used rather than dropping the county from the map.

ZORI is current but partial; ACS is complete but lagged. Both are offered, and the site never converts one into the other. A single national ZORI/ACS multiplier was tested against the 1,351 counties carrying both and rejected: correlation 0.675, median error 11%, ninetieth-percentile error 27%, with Pitkin County (Aspen) off by a factor of nine. An 11% error in rent moves the salary thresholds by roughly the same proportion, which is tens of thousands of dollars, so no county's rent is ever inferred from another source.

One consequence worth knowing: ACS gross rent includes utilities and ZORI does not. MIT's non-housing budget excludes them too, since they sit inside the housing row this project discards. So the ACS basis is the more complete measure of what a month costs, and the ZORI basis omits utilities altogether. That is perhaps $150 to $250 a month.

Everything except rent: MIT Living Wage Calculator

WhatFood, medical, transport, internet & mobile, civic, and other
HouseholdOne adult, no children
GeographyCounty
Coverage3,133 of 3,142 counties (99.7%)
RefreshAnnual, by MIT

MIT's figures are assembled from USDA food plans, MEPS medical expenditure data and BLS transport surveys. Crucially, the calculator's bottom line is a net requirement, meaning income needed after tax. That matches this site's net-based model exactly, with no risk of taxing the same dollar twice.

Its housing row is discarded. MIT prices housing from HUD Fair Market Rent, published annually; ZORI refreshes monthly, and keeping one live rent series is the entire point of this project. Mixing two rent definitions into one budget would double-count and stale the result at once.

The ingest re-derives MIT's own stated total from its component rows on every fetch and refuses the data if they disagree by more than $12. That check exists because the failure it catches, a shifted table column silently yielding a two-adult budget, produces numbers that look entirely plausible.

Tax: Tax Foundation, IRS and SSA

Federal2026 brackets and standard deduction (IRS Rev. Proc. 2025-32, post-OBBBA)
FICASSA contribution and benefit base; statutory Additional Medicare threshold
StateAll 50 states and DC, single-filer brackets and each state's own deduction

County boundaries: us-atlas

US Census Bureau cartographic boundaries at 1:10,000,000, as TopoJSON. Decoded, projected and flattened to SVG paths at build time so the browser does no geometry work and loads no mapping library.

The band model

For each county, the monthly cost of living is:

needs = rent  +  MIT non-housing essentials

where rent is whichever basis you have selected, Census ACS or Zillow ZORI. The rest of the model is identical either way.

Three salary thresholds are then defined by where that figure lands as a share of take-home pay. Every cut is a published budgeting standard rather than a number chosen because it looked reasonable:

ThresholdRuleWhere it comes from
Survival needs = 100% of take-home MIT's living wage is explicitly a subsistence budget: no savings, no travel, no meals out. Covering it exactly is the floor, not a target.
Getting by needs = 70% of take-home The 70/20/10 budget, leaving 20% for savings and 10% for debt or giving.
Comfortable needs = 50% of take-home The 50/30/20 rule, leaving 30% discretionary and 20% savings.

Why the thresholds are solved numerically

Going from "I need $4,600 a month after tax" back to a gross salary has no clean algebraic answer. Take-home is piecewise-linear in gross, with kinks at every federal and state bracket edge, at the Social Security wage cap, and at the Additional Medicare threshold. It is, however, strictly increasing, which is all bisection needs.

So grossForNet() bisects the tax engine to the cent. One implementation then covers every state, every bracket and both modelled city taxes automatically, and it stays correct when the tax year rolls. The test suite pins this by round-tripping salaries forward and back, including deliberately at the wage cap where a closed-form shortcut would go wrong.

Why the map is not simply painted with those four bands

At an entry-level tech salary, more than nine counties in ten fall into "comfortable". A four-class map would be a single colour almost everywhere and the encoding would carry no information. The underlying quantity is continuous and varies a great deal inside that band: necessities take about a quarter of take-home in rural Ohio and about two-thirds in the Bay Area.

The map therefore shades the continuous share, using the band boundaries as class breaks. The top three breaks are exactly the named standards (50%, 70%, 100%), so the map and the written verdict can never disagree; the lower breaks subdivide the crowded comfortable end where the real variation lives.

The model in symbols

Everything above, stated formally. These are the expressions the code evaluates, not a tidied-up version of them. Where the notation and the implementation once disagreed, the notation was corrected, and the three places that happened are called out below.

Variables

SymbolMeaning
B Annual base salary.
Grsu Total equity grant value, over the whole four-year vest.
vy Fraction of the grant vesting in year y, for example 0.05 for Amazon's 5% first year. The four values sum to 1.
Cy Cash bonus paid in year y, which is how a split sign-on is represented.
δ Equity haircut: the share of a grant counted as real. Not a universal 0.85. It is a property of the offer: 0.85 for the public-company presets, 0 for the Series B startup, because you cannot pay rent with illiquid private shares. It is also editable.
T(I,L) Total tax liability on gross wage income I in jurisdiction L. Expanded below.
Rm Median monthly rent. County, not ZIP code, and one bedroom by default, on whichever rent basis is selected.
Nc MIT's monthly non-housing essentials for one adult in that county.

The tax function includes payroll

T is often written as federal + state + city. That would leave out the largest single deduction after federal income tax. What the engine actually computes is:

T(I,L) = Tfed(I) + TFICA(I) + Tstate(I,L) + Tpayroll(I,L) + Tcity(I,L)

FICA alone is 7.65% up to the Social Security wage base: 6.2% Social Security capped at $184,500, 1.45% uncapped Medicare, plus 0.9% on wages over $200,000. The payroll term covers employee-side state levies such as California SDI and Washington's Cares and paid-leave premiums. Omitting either would overstate take-home by thousands a year on exactly the salaries this tool is aimed at.

Equity and liquid income

Equity counted in year y, after the haircut:

Ey= Grsu× vy×δ

Liquid monthly base, the headline figure, taxing base salary alone. This is the money that arrives on the same day every month, which is the only kind that reliably clears a rent payment:

Mbase= BT(B,L) 12

Liquid monthly total for year y, once bonus and vested equity are counted. Note that the tax function is applied to the combined figure rather than to each part separately. Vested RSUs are ordinary W-2 wage income in the year they vest, so they stack onto the same progressive base and push the marginal rate up:

Mtotal,y= (B+Cy+Ey) T(B+Cy+Ey,L) 12

Affordability ratios

The share of net monthly income that median rent consumes. Lower is more affordable; 0.30 is HUD's cost-burdened line.

ρbase= RmMbase ρtotal,y= RmMtotal,y

Both are left undefined, and drawn as a gap rather than as zero, whenever M0. An offer that nets nothing monthly cannot cover any rent, and rendering that as would put a bar off the end of a chart.

The band model

The ratios above answer a relative question. The county model answers an absolute one, and needs one more term for everything that is not rent:

nc= Rm+Nc σ= ncMbase

σ is the quantity the map shades and the verdict names. The three salary thresholds invert it. Writing the take-home function as G(x)=xT(x,L), the salary at which essentials take a share s is:

S(s)= G1 ( 12ncs ) , s {1.0,0.7,0.5}

G1 has no closed form, because G is piecewise-linear with kinks at every bracket edge, the wage cap and the Additional Medicare threshold, so it is evaluated by bisection, as described above. The three values of s are the survival, getting-by and comfortable lines.

Why the tax function has to be progressive

A flat effective rate is the shortcut that makes a model like this wrong in the one direction that matters. For tax year 2026 the federal standard deduction for a single filer is $16,100 and the brackets run from 10% up to 37% on income above $640,600, so a junior engineer on a $130,000 base and a staff engineer on $400,000 of total compensation face materially different effective burdens. Modelling both at one rate would flatter one and penalise the other. And since equity stacks onto the same progressive base, a flat rate would also erase exactly the year-to-year movement the vesting panels exist to show.

The tax engine

Single filer, standard deduction, wage income only. The engine holds no constants. Every figure lives in app/tax_data.py with the URL it came from, so rolling to a new tax year touches one file.

Philadelphia's wage tax is 3.735%. A search summary during development confidently reported 2.235%; the figure was checked against the City of Philadelphia's own publication and corrected. This is why every constant in the file carries a primary source, and why the two local taxes that are modelled are the two that could be verified against the taxing authority itself.

The map

Why counties, and why not states

An earlier draft shaded whole states by cost. That cannot work: Texas contains both Austin and Dallas, and California contains four metros that disagree with each other, so any state fill would have had to invent a number none of them supports. A choropleth is only honest when there is genuinely one value per shaded area, which is true of cost at county level and false at state level.

The state page carries the same map with a second view, and that view does shade whole states. What it shades is not cost. It is the spread between a state's dearest and cheapest county, which is a property of the state itself rather than a figure imputed to every square mile in it, so one fill is a true statement about the whole shaded area. The class breaks are fixed at 15, 25, 35, 45, 60 and 80 percent dearer, held constant across both rent bases so that changing the basis shows a change in the data rather than a change in the scale.

Projection

Albers USA: an equal-area conic projection for the lower 48, with Alaska and Hawaii as the conventional insets. Equal-area matters here because the map exists to compare places, and a Mercator-style projection would inflate the northern states enough to distort that comparison.

It is implemented from scratch in src/projection.js, because the deployed site carries zero runtime dependencies and loads no CDN. To make sure that "from scratch" did not mean "subtly wrong", the implementation was checked against d3-geo across 27 named locations and 4,982 grid points spanning the whole continental envelope, including the antimeridian, where the Aleutians cross from +179° to −179°. Agreement was exact to floating-point precision, and the fixtures generated from d3-geo are committed so the test suite keeps pinning it.

Zoom, and why the borders are simplified per arc

The map zooms to 32× with the scroll wheel, a pinch, the buttons, or the plus and minus keys, and one button frames whichever county is selected. All of it is a single transform on one group. At 3,142 shapes, anything that touches per-path geometry on a wheel event is a slideshow rather than a map.

Zooming exposed a flaw that had been invisible at full extent. Boundaries are simplified with Douglas-Peucker to keep the committed file small, and an earlier build simplified each finished county ring. Two neighbouring counties share the border between them, so that border was being approximated twice, from different vertex neighbourhoods, and the two polygons stopped agreeing on where it ran. At 1× the disagreement was sub-pixel. Zoomed in it opened into a white sliver between every pair of counties. Simplification now runs on the shared arc, which is the thing TopoJSON stores shared arcs for in the first place, so both counties are guaranteed the identical vertex list rather than merely a very similar one. It grew about 10%, which is what the correctness costs.

County names

Boundary files carry a bare name, "King" or "Orleans", so the official name comes from the Census county gazetteer instead. That matters because the suffix is not always "County": Louisiana has parishes, Alaska has boroughs and census areas, and Virginia has independent cities, so deriving it from the state would get all three wrong. The build fails if any county is missing a name rather than shipping a partial label.

Nine county-equivalents are named explicitly because the boundary file predates the gazetteer: Connecticut replaced its eight counties with planning regions in 2022, and Alaska split Valdez-Cordova in 2019. Zillow still reports on the old Connecticut counties, so the map keeps drawing them.

Grey counties

On the Zillow basis, more than half of US counties are drawn unshaded because Zillow publishes no rent index for them. They are mostly rural, with too little listing volume for a stable index. Switching to the Census basis fills them in with real measured figures. Nothing is ever estimated from neighbours or from a state average, because a map that guesses looks exactly like a map that knows.

A handful stay grey on both bases: Connecticut, whose counties no longer join MIT's planning-region geography, and about ten counties where the Census suppresses the rent estimate for confidentiality.

HUD's Fair Market Rents were the first candidate for complete coverage and are unusable here: huduser.gov answers automated requests with an AWS WAF challenge rather than data. That is worth stating plainly, because it is the same failure that left this project's original rent extractor silently serving four hardcoded rows for its entire existence. That is the bug that motivated rebuilding it to fail loudly.

How a state gets a number

Job posts, tax comparisons and relocation advice all talk in states, so the state page answers in states. It just refuses to do it by averaging. Every figure it shows is taken from a real county and labelled with that county's name, which means a reader can always go and check the place the number came from.

FigureHow it is produced
The median county Every county in the state that has a rent figure on the chosen basis is ranked by total monthly cost, and the one in the middle is reported by name. It is a selection, not a calculation: no value is averaged into existence.
The cheapest and dearest The two ends of that same ranking, also named. These bound what the state can honestly be said to cost.
The spread Dearest divided by cheapest. It is a measure of how misleading any single figure for that state would be, which is why the page leads with it.
Tax The same engine described above, run on the salary you enter. This is the one genuinely state-level quantity on the page, because income tax law really is written per state.

The median is over counties, not people. No population figure ships with this project, so a state's median county is its middle county by cost, not the county half its residents live below. In a state with many small rural counties and one large city, that pulls the middle down: California's median county is nothing like California's median resident. Treat it as the midpoint of the state's geography, which is what it is, and not as a typical experience of living there.

Coverage also moves the answer. On the Census basis nearly every county is present; on the Zillow basis only about 1,360 nationally, and they are the larger ones, so a state's range is drawn from its cities and comes out artificially narrow. The page says which basis it is on and how many counties it found.

Seasonality, and the "when to move" page

The county rent file carries 138 monthly columns going back to 2015, and the affordability model reads exactly two of them. The rest is a decade of history that answers a different question: not where to move, but when.

Classical multiplicative decomposition

For each county's monthly series:

trend[i]  = 12-month CENTRED moving average, half weight on the two end months
ratio[i]  = value[i] / trend[i]
index[m]  = mean of ratio[i] for every i falling in calendar month m
            normalised so the twelve indices average to exactly 1.0

The centring is what makes it work. A trailing average lags the series by six months and would smear the seasonal peak into the wrong months entirely. The half weight on the first and last month of the window keeps it spanning exactly twelve months, so no calendar month is counted twice.

The result reads directly: 0.98 for January means January rents sit about 2% below that year's own trend. Dividing by the trend, rather than subtracting it, is what separates the seasonal pattern from the long-run climb. A market growing 5% a year would otherwise show every autumn as "expensive" purely because it came later.

What is deliberately excluded

A calendar month needs at least three observations before it enters its average, so about half the counties in the rent file are shown with a trend chart but no seasonal pattern. Publishing a January estimate built from one January would be false precision dressed as insight.

How large the effect really is

Median peak-to-trough swing, among counties that clear the bar: about 2%. On a $2,000 rent that is roughly $40 a month. Real, worth knowing, and far smaller than the differences between places, which is why the page says so at the top rather than in a footnote.

ZORI is a smoothed index, and smoothing damps precisely the within-year movement being measured. These amplitudes are therefore conservative: the swing in raw listing prices is probably larger. That is the safe direction for an error to run, but it is still an error, and it is the reason no month is presented as a prediction.

A useful sanity check on the method: the counties with the largest swings are Newport County RI, Grand Traverse MI, Pitkin CO and Barnstable MA. Cape Cod, Aspen, and two lake resorts. Markets dominated by one short season are exactly where a seasonal index should light up, and it does.

Compensation

The presets are median reported packages by company and level from Levels.fyi, dated on the page. Base salary and the bonus total come from there directly; the equity grant is its annualised stock figure multiplied by four, which is the grant it was derived from.

The shape of each vesting schedule is the company's published one: Google's front-loaded 33/33/22/12, Amazon's back-loaded 5/15/40/40 with sign-on cash bridging the early years, even quarterly vesting elsewhere. Where a company front-loads cash to offset a back-loaded vest, the four-year totals still reconcile to Levels.fyi.

"Other", and why it is not in the data file

The Other chip is an offer that is nobody's published package. It deliberately does not live in data/profiles.json: every row in that file carries a source and a date, and a number you typed has neither, so mixing the two would quietly weaken the guarantee the rest of the file makes.

Choosing it keeps whatever is already in the fields, because adjusting from a real anchor beats retyping four numbers from zero. What it does replace is the vesting shape. A preset's schedule belongs to that company, and once the package is yours there is nothing left to justify assuming it, so the four-year view falls back to even quarters. Typing over a preset's numbers switches you to Other automatically, but keeps the schedule you started from and says so, rather than reshaping the curve underneath you. Either way your offer joins the comparison grid and the vesting panels alongside the sourced ones.

The headline uses base salary only

Total comp flatters the number exactly where a relocation decision can least afford it. A sign-on bonus is one lump in January; RSUs vest quarterly at a price nobody can predict. Neither pays next month's rent reliably, so equity and bonuses get their own views rather than being folded silently into the headline.

Equity is discounted, not counted at face value

Public-company RSUs take a 15% haircut for price risk between grant and vest. Private company shares default to a 0% haircut, not because they are worthless, but because you cannot pay rent with illiquid paper.

Pay is not location-adjusted, and that is deliberate

Real packages are location-adjusted: the same level is commonly 15–25% lower in a tier-3 metro than in the Bay Area. This site does not model that, which means dragging a Bay Area preset onto a cheap county overstates what you would actually be paid there.

It is left unmodelled because no authoritative per-metro table is published. The one credible public source of real filed salaries by employer and worksite, the Department of Labor's H-1B LCA disclosure data, returns HTTP 403 to automated requests. Deriving location factors from secondary aggregators would have meant shipping invented numbers with the same confidence as verified ones. The salary field is editable instead: put your actual offer in it.

Known limitations

These are the things this site gets wrong or cannot see. They are listed here rather than buried because a tool that hides its error bars is worse than no tool.

LimitationEffect
Local income tax is modelled for two cities only About a third of counties with data sit in states levying some local income tax. Only New York City and Philadelphia are computed. Affected counties carry an on-screen warning, and their take-home is overstated. Maryland and Indiana tax at county level and would fit this data model, but both authorities publish their schedules as PDFs that could not be read reliably, and Maryland moved two counties to tiered rates for 2026. Everywhere else the tax is municipal rather than county level, and Pennsylvania alone has some 2,500 rates, so no single county figure would be correct.
State paid-leave payroll levies cover CA and WA only About a dozen other states run employee-funded programmes of roughly 0.3–0.7% of wages. Take-home in those states is overstated by about half a percent of gross.
Single filer, standard deduction No dependents, no 401(k) deferral, no itemising, no spouse. Any of these moves your real number, usually by more than the differences the site is highlighting.
County-level, not neighbourhood King County covers both downtown Seattle and rural foothills at one rent figure. Within-county variation can exceed between-county variation.
Connecticut is unshaded Connecticut replaced its counties with planning regions in 2022. Zillow still reports on the old county geography and MIT uses the new one, so the two do not join. Rather than map one onto the other approximately, those eight counties are left without a cost figure.
The cost baseline is subsistence, not aspiration MIT's budget contains no savings, no holidays and no meals out. That is what makes it the right floor for "survival", and the reason "comfortable" adds 50% headroom on top rather than simply being a larger version of the same number.
Utilities are missing on the Zillow basis ZORI excludes them and MIT's budget puts them in the housing row this project discards, so a ZORI-based total understates the real monthly cost. The Census basis includes them.
Health insurance is a national-ish average MIT's medical figure does not know what your employer's plan costs you, which at large tech companies is usually well below average.
Compensation is a snapshot, not a feed Rent refreshes monthly and automatically. The comp presets are dated medians that a human updates.

Read the source instead

Everything above is a description of code you can go and read. There is no build step and nothing is minified, so the files in the repository are byte for byte the files your browser runs. That makes this page checkable rather than merely believable, and if the two ever disagree, the code is what is true. The model is about 1,700 lines of plain JavaScript in total.

The README has the commands to run it, refresh the data and execute the tests.

To check this claimRead this
The brackets, deductions and payroll rates are the current year's app/tax_data.py
Every constant carries the source it came from and the date it was read.
Marginal tax is walked correctly, and take-home is what is left app/tax.py
The three salary thresholds come out of the budgeting rules named above app/bands.py
A state's figures are a real county's, never an invented average app/state_rollup.py
The seasonal index is a centred moving average with the trend removed app/seasonality.py
Nothing is filled in when a source goes quiet scripts/
One ingest per source, each of which exits non-zero rather than guessing.
All of the above stays true tests/ and test/
144 assertions over the maths, the ingests and the claims on these pages.

Every ingest fails loudly

If a source is unreachable or its format changes, the script exits non-zero and the last good data stays committed: stale numbers, still labelled with their real date. No script substitutes placeholders, because a pipeline that silently falls back to hardcoded values looks identical to a working one for as long as nobody checks. That is not hypothetical: it is exactly what the version of this project that preceded the rewrite did, for its entire existence.

The tests pin the prose, not just the code

The unit suite asserts the factual claims the pages make about the reference offers: that Google's front-loaded grant decays, that Amazon's back-loaded one climbs past it, that the evenly-vesting offers stay flat. If the data is updated so a claim stops holding, the tests fail and flag the wording as stale. A sentence the numbers no longer support is a correctness bug, not a wording nit.