Jaros and Rauh (NBER, September 2026; first version by Rauh, Jaros, Kearney, Doran and Cosso, March 2026), reproduced step by step from the authors’ own public code and data

Published

October 17, 2026

R code for the setup
suppressPackageStartupMessages({
  library(targets); library(dplyr); library(knitr); library(ggplot2)
})
store <- "../_targets"                       # rjkdc-analysis/_targets
tr <- function(name) tar_read_raw(name, store = store)
for (f in list.files("../R", pattern = "\\.R$", full.names = TRUE)) source(f)

rc   <- tr("revenue_chain")
pf   <- tr("pareto_fit")
sch  <- tr("pareto_income_schedule")
t8   <- tr("income_tax_table8")
itmc <- tr("income_tax_mc")
t9   <- tr("npv_table9")
t10  <- tr("npv_table10")
mcs  <- tr("npv_mc_ssrn")
mcn  <- tr("npv_mc_nber")
nc   <- tr("nber_ceiling")
ncol <- tr("nber_collectible")
hl   <- tr("site_headlines")
grid <- tr("site_grid")
# the explorer's dials that are the authors' Monte Carlo inputs; q and g are this OPA's settings
own_dials <- Filter(function(d) !d$key %in% c("q", "g"), site_dials(rc))
stopifnot(length(site_dials(rc)) - length(own_dials) == 2)   # the prose names the two
inputs  <- read.csv("../export/r/inputs.csv")
outputs <- read.csv("../export/r/outputs.csv")
printed <- read.csv("data/printed-tables.csv")
ss <- formals(compute_npv_mc_ssrn); nb <- formals(compute_npv_mc_nber)
# HTML tables: pandoc would read a pair of "$" across markdown table cells as math
options(knitr.table.format = "html")

# formatting: a real minus sign, fixed decimals
num <- function(x, d = 1) {
  s <- formatC(abs(x), format = "f", digits = d, big.mark = ",")
  ifelse(x < 0, paste0("−", s), s)
}
pct <- function(x, d = 1) paste0(num(x, d), "%")
# −$38.9B; a word joiner (U+2060) after the sign keeps a line from breaking between "−" and "$"
usdb <- function(x, d = 1) paste0(ifelse(x < 0, "−\u2060", ""), "\\$", num(abs(x), d), "B")
out <- function(id, col = "value") outputs[[col]][outputs$output_id == id]
inp <- function(id) inputs$value[inputs$input_id == id]
h <- function(v, q, col = "reproduced") hl[[col]][hl$version == v & hl$quantity == q]
# raw HTML block: pandoc must not read "$B ... $B" inside a table as a math span
scroll <- function(x) cat("\n\n```{=html}\n<div class=\"table-scroll\">\n",
                          as.character(x), "\n</div>\n```\n\n", sep = "")

Introduction

Jaros and Rauh estimate that Proposition 40 leaves California poorer: a mean net present value of −⁠$38.9B (PDF p.1). The tax is collected once, but billionaires who leave stop paying income tax for ever. At the authors’ discount rates that lost stream is worth about $75B today, against $36B, the average revenue in the authors’ simulation (half of 72). The chapter’s own revenue estimate is lower, about $30B, and it sets that against the $104B benchmark of the BSZ analysis (PDF p.3). This report rebuilds the net present value step by step.

Following the Open Policy Analysis framework:

  • Open Output: the explorer, the mean net present value and share of negative outcomes for every combination of seven dials: the five ranges of the authors’ Monte Carlo and two settings this OPA adds
  • Open Analysis: this document, and the slide deck
  • Open Materials: the repository, every script, test, CSV and source (guide, with all ten reproduction notes kept along the way: the materials page)

What is reproduced. Jaros and Rauh, The Net Present Value of the California Billionaire Tax Act, NBER chapter c15504 (Tax Policy and the Economy, vol. 41; 22 September 2026; NBER is the National Bureau of Economic Research, which circulates working papers), the litigation-weighted revision of the first version by Rauh, Jaros, Kearney, Doran and Cosso (SSRN 6340778, March 2026; SSRN is a preprint site). Its headline: revenue of about $30B ($28.4B to $35.6B) and a mean NPV of −⁠$38.9B, with 85.2% of simulated outcomes negative (the Figure 3 note, PDF p.35; the text on p.1 says 85%). The authors report the share below zero as the share of plausible parameter combinations with a negative net present value (PDF p.35). Their draws are independent and uniform over ranges that are themselves guesswork, so the share is not a probability that the tax loses money: it moves with those ranges, as the explorer’s Rate 1.5% and Rate 4.5% settings show. The mean NPV is the main estimate, derived step by step in the next section.1

The analysis is reproduced from the authors’ own public, MIT-licensed code and data (github.com/bjaros20/wealth_tax, pinned at commit 25e84dd): their Excel workbook is read cell by cell, their R scripts are ported line by line and also re-run unmodified, and the person-level file final.csv is summed directly. Every number in this document is read from the project’s {targets} pipeline at render time ({targets} is an R package that runs the pipeline’s steps in order and records which outputs are current); the only numbers typed in are the paper’s printed values this OPA compares against, each with its PDF page (page numbers are PDF pages throughout).

The project’s pre-specified output (PLAN.md, 23 September 2026) reported the March 2026 and September 2026 versions side by side, with the March 2026 mean as the comparison’s anchor. On 14 October 2026, after results, the September 2026 mean became the main estimate and the comparison’s anchor, because the pages follow each analysis’s latest version (PLAN.md, decisions 7 and 9).

The shape of the argument, in one line: net present value = wealth tax collected once, minus the present value of the income tax that departing billionaires would have paid in every future year. Sections 4 and 5.1 of the paper estimate the two pieces; Sections 5.2 to 5.4 combine them. This report follows that order.

Notation, in one place

Symbol Meaning Unit
\(\tau\) statutory wealth tax rate share
\(WT\) one-time wealth tax revenue actually collected $B
\(C\) annual California income tax paid by the billionaires $B per year
\(f\) share of that income tax base that leaves the state share
\(r - g\) the interest rate minus the lost tax’s yearly growth (a real rate; the Monte Carlo’s draw) per year
\(g\) growth of the lost income tax stream; the explorer’s further growth on top of the draw, \(\Delta g\), is this OPA’s illustration per year
\(\varepsilon\) migration semi-elasticity: % of the base lost per percentage point of tax
\(q\) probability the Act survives constitutional challenge probability
NPV \(WT - f \cdot C/(r-g)\), the net fiscal effect today $B

The main estimate, step by step

The main estimate is the paper’s mean net present value, printed as −⁠$38.9B (PDF p.1 and p.35). The authors get it from a Monte Carlo: they draw four inputs at random 100,000 times, compute \(NPV = WT - f \cdot C / (r - g)\) for each draw, and average. Because the four draws are independent, the average has an exact value too, and this report builds it one input at a time. Every number below is read from the pipeline (R/npv_mc.R, R/nber_final.R); the draw ranges are the defaults of compute_npv_mc_nber(), which copies the authors’ script NPV_dist_v8.R.

Step 1: the wealth tax collected, \(WT\)

The script draws \(WT \sim U[0, 72]\) ($B). The top, 72, is the revenue if the Act survives in court, after removing the seven publicly reported moves. Summing the authors’ person-level file final.csv gives $72.05B for the same quantity. The script’s 72 is that value rounded, as the chapter rounds it on PDF pp.24 and 33 (note 4). The bottom, 0, is the Act struck down. The chapter also grounds the 0 in its stress test of unreported departures, in which revenue falls to $4.82B (fn.32, PDF p.34; Appendix B, PDF p.43). The mean of the draw is \[E[WT] = \frac{0 + 72}{2} = 36.00.\] This is the paper’s central revenue, \(q \times 72\) with a survival chance \(q\) = 0.50 (PDF p.33). The 0 lower bound is guesswork.

Step 2: the share of the income tax base that leaves, \(f\)

The script draws \(f \sim U[0.3, 0.6]\), on its own, unrelated to the revenue draw. Its mean is \(E[f]\) = 0.45: on average, 45 cents of every dollar of billionaire income tax leaves the state. The range is guesswork, “centered conservatively around the observed wealth-base departure fraction” (PDF p.31).

Step 3: the income tax the billionaires pay each year, \(C\)

The script draws \(C \sim U[3.3, 5.8]\) ($B a year), the two ends of the income tax Monte Carlo in Section 5.1. Its mean is \(E[C]\) = 4.55.

Step 4: the discount rate net of growth, \(r - g\)

The script draws \(r - g \sim U[0.015, 0.045]\), the interest rate minus the lost tax’s yearly growth (a real rate), between 1.5% and 4.5% a year. A stream of $1 a year, growing at \(g\) for ever, is worth \(1/(r-g)\) today, so what enters is the mean of \(1/(r-g)\), not \(1/E[r-g]\): \[E\!\left[\tfrac{1}{r-g}\right] = \frac{\ln(0.045 / 0.015)}{0.045 - 0.015} = 36.62.\] In words: at these rates, $1 lost this year, and growing every year for ever, is worth about $37 today. The range is guesswork. Its 1.5% anchor is a published market yield, but the width of the band, to 4.5%, is the authors’ allowance for illiquidity, mean reversion in entrepreneurial returns and a baseline outflow of high earners of about 1% a year (PDF p.32), not an estimate, and the width is what moves the result.2

Step 5: the present value of the income tax lost

The three loss inputs are independent, so the mean of their product is the product of their means: \[E\!\left[\frac{f\,C}{r-g}\right] = 0.45 \times 4.55 \times 36.62 = 74.98.\]

Step 6: the net present value

The revenue of Step 1 minus the loss of Step 5: \[E[NPV] = E[WT] - E\!\left[\frac{f\,C}{r-g}\right]\] \[= 36.00 - 74.98 = −38.98.\]

The authors do not print this exact expectation. They print the average of one seeded run of 100,000 draws. A seeded run starts the random draws from a fixed number (2026), so it gives the same draws every time. This OPA’s R port replays it draw for draw: −⁠$38.886B, printed as −38.9. The gap, 0.09, is less than one Monte Carlo standard error (0.12). The overview and the explorer show the printed −38.9; the explorer’s grid holds the exact value for every other setting. The explorer moves each of the four draws, and two settings the authors’ code does not have: a litigation multiplier on top of the draw, and growth of the lost income on top of the drawn \(r - g\) (this OPA’s illustration).

R code for the main estimate
# R/npv_mc.R: the seeded Monte Carlo (the printed number) and the exact expectation.
compute_npv_mc_nber <- function(seed = 2026, n_sims = 100000,
                                 wt_min = 0, wt_max = 72,
                                 f_min = 0.30, f_max = 0.60,
                                 c_min = 3.3, c_max = 5.8,
                                 rg_min = 0.015, rg_max = 0.045) {
  set.seed(seed)
  wt <- stats::runif(n_sims, wt_min, wt_max)
  f  <- stats::runif(n_sims, f_min, f_max)
  c_income <- stats::runif(n_sims, c_min, c_max)
  rg_discount <- stats::runif(n_sims, rg_min, rg_max)

  npv <- wt - (f * c_income) / rg_discount

  list(
    draws = tibble::tibble(wt = wt, f = f, c_income = c_income,
                            rg_discount = rg_discount, npv = npv),
    summary = npv_mc_summary(npv)
  )
}

npv_mc_summary <- function(npv) {
  tibble::tibble(
    n = length(npv),
    mean = mean(npv),
    median = stats::median(npv),
    sd = stats::sd(npv),
    p05 = unname(stats::quantile(npv, 0.05)),
    p95 = unname(stats::quantile(npv, 0.95)),
    pct_negative = 100 * mean(npv < 0)
  )
}

npv_e_inv_rate <- function(lo, hi, g = 0) {
  if (hi > lo) log((hi - g) / (lo - g)) / (hi - lo) else 1 / (lo - g)
}

npv_expectation <- function(wt_min, wt_max, c_min, c_max, rate_min, rate_max,
                            f_spec, q = 1, g = 0) {
  e_wt <- (wt_min + wt_max) / 2
  e_f <- switch(f_spec$mode,
    implied = 1 - e_wt / f_spec$baseline,
    uniform = (f_spec$lo + f_spec$hi) / 2,
    fixed   = f_spec$value
  )
  pv_lost <- e_f * mean(c(c_min, c_max)) * npv_e_inv_rate(rate_min, rate_max, g)
  collected <- q * e_wt
  c(mean_npv = collected - pv_lost, wt_collected = collected, pv_lost = pv_lost)
}

# _targets.R: the targets this section reads
  tar_target(nber_ceiling, compute_nber_ceiling(nber_final)),
  tar_target(npv_mc_nber, compute_npv_mc_nber()),
  tar_target(npv_mc_nber_summary, npv_mc_nber$summary),
  tar_target(npv_mc_nber_analytic_mean,
  tar_target(export_inputs, compute_export_inputs(pareto_fit, revenue_chain, nber_ceiling)),
                                                     npv_mc_nber_summary, nber_ceiling)),
             compute_site_headlines(site_grid, npv_mc_ssrn_summary, npv_mc_nber_summary,
                                    npv_mc_ssrn_analytic_mean, npv_mc_nber_analytic_mean,
Python code for the main estimate
# py/npv_mc.py: the Python twin. numpy's random stream differs from R's, so its seeded
# mean agrees with R's within Monte Carlo error; the exact expectation agrees exactly.
def compute_npv_mc_nber(seed=2026, n_sims=100000, wt_min=0, wt_max=72,
                         f_min=0.30, f_max=0.60, c_min=3.3, c_max=5.8,
                         rg_min=0.015, rg_max=0.045):
    rng = np.random.default_rng(seed)
    wt = rng.uniform(wt_min, wt_max, n_sims)
    f = rng.uniform(f_min, f_max, n_sims)
    c_income = rng.uniform(c_min, c_max, n_sims)
    rg_discount = rng.uniform(rg_min, rg_max, n_sims)

    npv = wt - (f * c_income) / rg_discount
    return {"npv": npv, "summary": npv_mc_summary(npv)}

def npv_mc_summary(npv):
    return {
        "n": len(npv),
        "mean": float(np.mean(npv)),
        "median": float(np.median(npv)),
        "sd": float(np.std(npv, ddof=1)),
        "p05": float(np.percentile(npv, 5)),
        "p95": float(np.percentile(npv, 95)),
        "pct_negative": float(100 * np.mean(npv < 0)),
    }

def npv_mc_analytic_mean(wt_min, wt_max, c_min, c_max, rate_min, rate_max,
                          f_mode="ssrn", baseline_revenue=None, f_min=None, f_max=None):
    e_wt = (wt_min + wt_max) / 2
    e_c = (c_min + c_max) / 2
    e_inv_rate = math.log(rate_max / rate_min) / (rate_max - rate_min)
    if f_mode == "ssrn":
        e_f = 1 - e_wt / baseline_revenue
    else:
        e_f = (f_min + f_max) / 2
    return e_wt - e_f * e_c * e_inv_rate

Section 4: the wealth tax revenue

This section builds the revenue. At 5% of the base, it is $94.30B before anyone leaves and $100.91B at the valuation date. Removing the seven publicly reported moves leaves $72.05B. Leaving out three pre-filing departures and Zuckerberg, and halving for the chance the Act is struck down, gives the paper’s about $30B (p.23).

4.1 The baseline, before anyone leaves (eqs. 1 and 2)

Take each of the \(N\) = 212 California billionaires on the 2025 Forbes list (with the authors’ corrections), subtract their directly held residential real estate (TRRE, total residential real estate; the Act exempts it), multiply what is left by the 5% rate, and add up. Their aggregate net worth is $1,894.8B. The authors’ workbook sums this row by row to $94.30B; the paper prints $94.20B (PDF p.19, eq. 2), a value typed into the workbook, 0.1% below its own sum. Every later number in the first version is built on the typed value, and each reproduces (note 6).

\[R_{\text{base}} = \sum_{i=1}^{N} \tau \times \text{TaxBase}_i, \qquad \text{TaxBase}_i = \text{NetWorth}_i - \text{TRRE}_i\]

The tax is assessed on net worth at 31 December 2026, so the authors grow every net worth by 7% to that valuation date and rebuild the base person by person in final.csv: 212 domestic rows, face value $100.91B (printed $100.9B, PDF p.19). That is exactly 1.0700 times the workbook’s $94.30B, so the person-level file and the workbook agree with each other, and both differ from the printed $94.20B. The 28 non-US-citizen residents ($146.3B of net worth) stay outside the base (PDF p.17).

4.2 The collectibility principle (eq. 3)

The authors count revenue only when it is “more likely than not” to be collected.

\[E[R] = q \times R_{\text{recognized}}\]

They borrow this test from the accounting rule for uncertain tax positions in company accounts (ASC 740-10, PDF p.20). It works at two levels. For each taxpayer: publicly reported moves, pre-filing departures and billionaires imputed from a Pareto curve are left out of the recognized base, \(R_{\text{recognized}}\). For the Act as a whole: \(q\) is the probability that the Act survives constitutional challenge, set at 0.50 (PDF pp.19-20). That value comes from the accounting standard’s 50% threshold, not from a legal assessment of the challenges, so it is guesswork.

4.3 The seven publicly reported moves (Table 7, eq. 4)

The chapter calls them “publicly confirmed pre-snapshot departures” (Table 7, PDF p.21). This OPA says “publicly reported moves”, because whether each ended legal residence in time is disputed (disputes row 3). Table 7 lists Page, Brin, Thiel, Hankey, Spielberg, Kalanick and Sacks as having moved out between the initiative’s filing and the 1 January 2026 residency date (PDF p.21). Removing their $28.85B leaves $72.05B in final.csv. The paper (eq. 4, p.22) and the authors’ README print $72.06B, the difference of two figures each rounded to two decimals (100.91 − 28.85), and the script and the chapter’s later pages use 72 (note 4): one value at three roundings. The paper then asks how big a behavioural response that is. It divides the share of revenue lost by the rate change, in percentage points (eq. 4, PDF p.22):

\[\varepsilon = \frac{\%\Delta \text{TaxBase}}{\Delta \text{TaxRate (p.p.)}} = \frac{(100.9 - 72.06)/100.9}{5}\]

R code for the implied semi-elasticity
eps7 <- 100 * (1 - nc$ceiling_recomputed / nc$domestic_total_grown) / (100 * inp("tax_rate"))
eps7
[1] 5.71903

This gives 5.72. The printed 5.73 is the same formula with the round ceiling, (100.9 − 72)/100.9/5 = 5.73, so the two differ only in rounding (note 4). A 1-point tax rate rise drives out about 5.7% of the base. The authors treat this as a floor, because it counts only departures that made the news (PDF pp.22-24).

4.4 The litigation discount and the collectible estimate (eqs. 5 to 8)

Removing three pre-filing departures as well ($0.88B) leaves $71.18B (printed $71.17B, PDF p.22). Mark Zuckerberg announced his move in February 2026, after the residency date. The authors exclude his $12.03B, because collecting it depends on the retroactive date surviving in court. That leaves a recognized base of $59.15B (eq. 5). Multiplying by \(q\) = 0.50 gives

\[E[R] = q \times R_{\text{recognized}} = 0.50 \times 59.15 = 29.57\]

(printed $29.6B, eqs. 6 and 8, PDF p.23). Keeping Zuckerberg gives $35.59B (eq. 7). Moving the gray-zone names out gives $28.40B: these are the seven billionaires with departure signals short of confirmation, plus Jan Koum, Reed Hastings and Andy Fang, reported as considering a move (PDF p.23). Hence the printed range, $28.4B to $35.6B, around “approximately $30 billion”.

The Monte Carlo does not use this point estimate. It draws revenue from 0 to 72, whose mean is the paper’s central $36B (Step 1 and Section 5.4).

4.5 Validation against the literature (Table 8)

Brülhart et al. (2022) estimate that Swiss taxable wealth falls by about 43% per point of wealth tax, about 24% of it through people moving.

\[\eta_{\text{mig}} = 0.24 \times 43 = 10.32, \qquad \text{base lost} = 1 - (1 - 0.05)^{10.32}\]

R code for the literature-implied base loss
lit_loss <- 1 - (1 - inp("tax_rate"))^inp("brulhart_semi_elasticity")
lit_loss
[1] 0.4110104

The authors keep only the moving part, 10.32, and apply it to the 5-point rate in log form: the base shrinks by 41.1% (PDF p.24). Table 8 (PDF p.25) sets this beside the observed 28.6% from the seven publicly reported moves. This check does not enter the main estimate. Two things to notice. First, the Swiss estimate is for a recurring annual tax. The paper argues that taxpayers price the Act as the start of a recurring regime, so the recurring-tax estimate is the right benchmark (PDF p.25). Second, the BSZ analysis (Boll, Saez and Zucman) disputes exactly this transfer (disputes row 1).

Section 5.1: the income tax the billionaires pay

This section estimates \(C\), the income tax the billionaires pay each year. It is at most $5.8B if they are the 212 highest earners and about $3.3B if they are spread among the top 500 filers. The Monte Carlo draws between the two.

The Pareto upper bound (eqs. 9 and 10)

The state publishes how many tax returns report income above $1M, $2M, $3M, $4M, $5M and $10M (Franchise Tax Board, FTB below: Personal Income Tax Annual Report 2024, Table B-4A). If top incomes follow a Pareto distribution, the log of those counts falls on a straight line against the log of the thresholds, with slope \(-\alpha\). The fitted line gives \(\hat\alpha\) = 1.438 (printed 1.44, \(R^2\) = 0.9987, PDF p.26).

\[N(y) = A\,y^{-\alpha}, \qquad S(k, n) = \left(\frac{k}{n}\right)^{(\alpha - 1)/\alpha}\]

R code for the Pareto fit figure
d <- data.frame(y = pf$thresholds, n = pf$filer_counts)
ggplot(d, aes(y, n)) +
  geom_abline(intercept = log10(pf$A), slope = -pf$alpha, linetype = 2, colour = "grey50") +
  geom_point(size = 2.5, colour = "#1f6b3b") +
  scale_x_log10(labels = scales::label_dollar(scale = 1e-6, suffix = "M")) +
  scale_y_log10(labels = scales::label_comma()) +
  labs(x = "AGI threshold", y = "Returns above the threshold") +
  theme_minimal(base_size = 11)

The six published filer counts against their AGI thresholds, log-log, with the fitted Pareto line (paper Figure 1, PDF p.27).

\(S(k, n)\) is the share of the bracket’s income that goes to its top \(k\) of \(n\) filers. With the top 212 of the 4,729 filers above $10M, the continuous formula gives 38.8% of the bracket’s $11.1B of tax. The script then builds a rank-by-rank income schedule and rescales it by 1.118 so that its top 212 match the continuous share (footnote 28, PDF p.28: the discrete sum undershoots by about 11%). Scaling Tax Year 2023 to FY 2024-25 collections ($130B in total) multiplies by 1.335. If the billionaires are the 212 highest earners, they pay $5.76B a year. That is the upper bound, and the top of \(C\)’s range.

The dispersion Monte Carlo (Table 9)

Billionaires are ranked by wealth, not income, and many realise little income in a given year. So the script places the 212 billionaires at random among the top \(K\) filers (without replacement, 100,000 draws for each \(K\) from 212 to 2,000) and sums their tax. The wider the pool, the lower the expected total. The authors take \(K \approx 500\) (“roughly the top decile of the bracket”, PDF p.28) as a reasonable floor, which gives the bottom of \(C\)’s range, $3.3B. The choice of \(K\) = 500 is a judgement, not an estimate, so the range \(C \in [3.3, 5.8]\) is labelled guesswork below.

R code for Table 9
p8 <- printed[printed$table == "table8", ]
pv <- function(k, col) p8$printed[p8$row == k & p8$column == col]
tab8 <- t8 %>% transmute(
  `Pool size K` = format(K, big.mark = ","),
  `Mean` = num(mean_fy25, 3), `printed` = sapply(K, pv, "mean"),
  `P5` = num(p05_fy25, 3), `printed ` = sapply(K, pv, "p05"),
  `P95` = num(p95_fy25, 3), `printed  ` = sapply(K, pv, "p95"))
m8 <- sum(round(t8$mean_fy25, 2) == sapply(t8$K, pv, "mean")) +
      sum(round(t8$p05_fy25, 2) == sapply(t8$K, pv, "p05")) +
      sum(round(t8$p95_fy25, 2) == sapply(t8$K, pv, "p95"))
scroll(kable(tab8, align = "rrrrrrr", caption = "Table 9, $B per year (PDF p.30)"))
Table 9, $B per year (PDF p.30)
Pool size K Mean printed P5 printed P95 printed
212 5.757 5.76 5.757 5.76 5.757 5.76
250 5.181 5.18 4.785 4.79 5.453 5.45
300 4.609 4.61 4.136 4.14 5.007 5.01
400 3.828 3.83 3.331 3.33 4.314 4.31
500 3.308 3.31 2.829 2.83 3.818 3.82
750 2.536 2.54 2.116 2.12 3.033 3.03
1,000 2.099 2.10 1.726 1.73 2.572 2.57

21 of the 21 cells round to the printed value; the tests hold this at the printed rounding (tests/testthat/test-income-tax-mc.R).

Section 5.2: from an annual loss to a present value (eqs. 11 to 14)

This is the step that makes the result. The wealth tax is collected once. The income tax of anyone who leaves is lost every year, for ever, and a perpetuity paying \(C\) a year is worth \(C/r\) today.

\[PV = \frac{C}{r}, \qquad NPV = WT - f \cdot \frac{C}{r}\]

\[\text{or, with growth:}\qquad PV = \frac{C}{r - g}, \qquad NPV = WT - f \cdot \frac{C}{r - g}\]

At a rate of 1.5% a perpetuity is worth 1/0.015, about 67 years of payments; at 4.5%, about 22. The paper’s text then argues the lost stream grows with the billionaires’ wealth, making it a growing perpetuity, \(C/(r-g)\) (PDF p.31). It calibrates \(r - g\) at about 1.5% from the S&P 500’s dividend yield, and presents scenarios at 1.5%, 3.0% and 4.5% (PDF p.32).

Three things are worth knowing before reading the rest:

  • Which rate is drawn. The chapter’s eq. 18 and its code (NPV_dist_v8.R) draw the 1.5% to 4.5% rate as \(r - g\), the discount rate net of growth, and the NBER README describes the draw the same way. One sentence after eq. 19 writes \(C/r\) (PDF p.35) (note 3). The first version’s code divides by plain \(r\) (note 2). The explorer’s dial for further growth on top of the drawn \(r - g\) is this OPA’s illustration, not part of the authors’ model.
  • The loss is not scaled by \(q\). People who left do not come back if the Act is struck down, so the income tax loss stays even when the wealth tax collects nothing (PDF p.33).
  • The horizon. The authors assume leavers do not come back, because the Act permanently lifts the constitutional cap on taxes on intangible property and lets the Legislature change the Act by a two-thirds vote (chapter PDF p.32). Treating the loss as permanent is one of the disputed choices: the BSZ analysis argues the loss should be temporary and gives five years, the payment period, as an example (BSZ PDF p.27) (disputes row 2).

Section 5.3: the break-even departure share (Table 10, eq. 15)

Set NPV to zero and solve for the departure share. The tax pays for itself only if less than \(x^*\) of the income tax base leaves.

\[x^* = \frac{WT \cdot (r - g)}{C}\]

The paper evaluates it at its central revenue, $36B (\(q \times 72\)), and the midpoint income tax, $4.55B (PDF p.33).

R code for Table 10
wt36 <- 0.5 * nb$wt_max
n10 <- compute_npv_table10(scenarios = list(central = list(label = "Central", WT = wt36, C = 4.55)))
pn <- printed[printed$table == "nber_table10", ]
mn10 <- sum(abs(round(100 * n10$break_even_pct, 1) - pn$printed[match(paste0("central@", n10$r), pn$row)]) < 1e-9)
scroll(kable(n10 %>% transmute(`WT ($B)` = num(WT, 0), `C ($B)` = num(C, 2), `r - g` = pct(100 * r),
                               `x*` = pct(100 * break_even_pct),
                               printed = pct(pn$printed[match(paste0("central@", r), pn$row)])),
             align = "rrrrr", caption = "Table 10 (PDF p.34)"))
Table 10 (PDF p.34)
WT ($B) C ($B) r - g x* printed
36 4.55 1.5% 11.9% 11.9%
36 4.55 3.0% 23.7% 23.7%
36 4.55 4.5% 35.6% 35.6%

3 of the 3 cells match the printed digit. The central departure share, \(E[f]\) = 0.45, is above every one of them, which is why the mean NPV comes out negative (PDF p.34). A check that is not an input: plugging each \(x^*\) back into the NPV formula must give exactly zero (see internal identities).

Section 5.4: the Monte Carlo behind the headline (eqs. 16 to 19)

Draw the four inputs independently and uniformly, 100,000 times, and average the NPV (PDF p.35).

\[WT \sim U[\$0\text{B}, \$72\text{B}], \quad C \sim U[\$3.3\text{B}, \$5.8\text{B}], \quad (r - g) \sim U[0.015, 0.045], \quad f \sim U[0.30, 0.60]\]

Two details matter:

  1. Revenue is drawn from zero. The mean of \(U[0, 72]\), $36B, is \(0.5 \times 72\), the paper’s central estimate (PDF p.33). This is the only place \(q\) appears in the code: there is no explicit multiplier (note 5). The script’s comment says the draw “embeds litigation range”, and the chapter also grounds the 0 in unreported departures (fn.32, PDF p.34). A uniform draw from 0 is a different assumption from a survive-or-not coin flip on a fixed base. The two share a mean, not a distribution.
  2. The departure share is drawn on its own, not tied to revenue. Revenue lost and income tax lost do not move together in a draw.
R code for Section 5.4
s <- tr("npv_mc_nber_summary")
scroll(kable(data.frame(
  ` ` = c("reproduced (seed 2026)", "printed (PDF p.35)"),
  `Mean` = c(num(s$mean, 2), num(h("nber", "mc_mean_npv", "printed"))),
  `Median` = c(num(s$median, 2), num(h("nber", "mc_median_npv", "printed"))),
  `SD` = c(num(s$sd, 2), num(h("nber", "mc_sd_npv", "printed"))),
  `% negative` = c(num(s$pct_negative, 2), num(h("nber", "mc_pct_negative", "printed"))),
  check.names = FALSE), align = "lrrrr",
  caption = "NPV Monte Carlo, $B, seed 2026, 100,000 draws"))
NPV Monte Carlo, $B, seed 2026, 100,000 draws
Mean Median SD % negative
reproduced (seed 2026) −38.89 −35.26 37.62 85.17
printed (PDF p.35) −38.9 −35.3 37.6 85.2

This OPA’s R port replays the authors’ script exactly (same seed, same draw order) and agrees with an unmodified run of NPV_dist_v8.R to machine precision (tests/testthat/test-npv-mc.R). The printed summary reproduces to within Monte Carlo error, with no systematic gap. Its exact expectation is −⁠$38.98B, which rounds to −39.0 rather than the printed −38.9: the printed value is the seeded run’s, not the expectation. The explorer shows the exact expectations and says so.

R code for the NPV distribution figure
ggplot(data.frame(npv = mcn$draws$npv), aes(npv, fill = npv < 0)) +
  geom_histogram(binwidth = 5, boundary = 0) +
  geom_vline(xintercept = mean(mcn$draws$npv), linetype = 2) +
  scale_fill_manual(values = c(`TRUE` = "#9a9a9a", `FALSE` = "#555555"), guide = "none") +
  coord_cartesian(xlim = c(-250, 70)) +
  labs(x = "NPV, $B", y = "draws") + theme_minimal(base_size = 11)

The distribution of simulated NPV (paper Figure 3, PDF p.35). Draws below zero in the lighter gray; the dashed line is the mean.

Every input, by origin

This section is not in the paper: it re-sorts every input by how much evidence stands behind it, the question a reader who wants to argue with the result has to answer first.

The headline: 4 of the authors’ 5 inputs that move the result in the explorer are guesswork. The explorer’s other two dials (an extra litigation weight and further growth) are this OPA’s settings. The one labelled data, the revenue ceiling, rests on a legal judgement about who counts as having left (disputes row 3). The data and research inputs (the Forbes base, the FTB brackets, the Pareto fit) fix the scale. Whether the NPV is negative is decided by the guesswork: how wide the revenue draw is, where the income tax range stops, which rate is used, and whether the lost income tax is permanent.3

  • data: read off a public dataset or the authors’ own person-level file, and recomputable from it.
  • research: an estimate taken from a published paper.
  • guesswork: set by assumption. Some are bounded by evidence, but no dataset or estimate pins the value itself.
  • scenario: a choice about which policy or counterfactual is being estimated.
  • derived: a value another row implies.
R code for the input table
dials <- site_dials(rc)
dial_rows <- do.call(rbind, lapply(dials, function(d) data.frame(
  input = if (d$key == "g") d$sym else d$key, origin = d$origin, what = d$label, `value(s)` = paste(d$labels, collapse = "; "),
  where = "explorer dial (R/site.R)", check.names = FALSE)))
# The rows only the first version uses (version "ssrn") go to the appendix.
first_only <- inputs$version == "ssrn"
inp_row <- function(x) data.frame(input = x$input_id, origin = x$label, what = x$description,
  `value(s)` = ifelse(is.na(x$value), "1.5% to 4.5%", vapply(x$value, function(v) format(signif(v, 6)), "")),
  where = x$source, check.names = FALSE)
odf <- rbind(inp_row(inputs[!first_only, ]), dial_rows)
scroll(kable(odf, caption = "Inputs by origin (export/r/inputs.csv, then the explorer's dials)"))
Inputs by origin (export/r/inputs.csv, then the explorer's dials)
input origin what value(s) where
n_billionaires data CA billionaires in the domestic base 212 CA_Billionaires_Revenues_and_Migration_final.xlsx!Summary_Preferred!D15
baseline_net_worth data Aggregate net worth, 212 domestic billionaires 1894.8 CA_Billionaires_Revenues_and_Migration_final.xlsx!Summary_Preferred!E15
tax_rate data Statutory wealth tax rate 0.05 CA_Billionaires_Revenues_and_Migration_final.xlsx!Calculations_Preferred!N3
pareto_alpha research Pareto tail parameter fit to FTB AGI thresholds 1.43789 monte_carlo_sim.R:33-44 (FTB PIT Annual Report 2024, Table B-4A)
bracket_tax_ty23 data TY2023 tax liability, $10M+ AGI bracket 11.1 monte_carlo_sim.R:51 (FTB Table B-4A)
total_pit_fy25 data Total FY2024-25 PIT collections 130 monte_carlo_sim.R:89 (CA State Controller cash receipts)
rauh_mc_c_min guesswork Annual CA income tax of billionaires: lower bound of the draw 3.3 NPV_dist.R; paper eq.23 (from the K=500 dispersion draw); NPV_dist_v8.R:21 (NBER chapter p.1, pp.29-31)
rauh_mc_c_max guesswork Annual CA income tax of billionaires: upper bound of the draw 5.8 NPV_dist.R; paper eq.23 (Pareto upper bound); NPV_dist_v8.R:21 (NBER chapter p.1, pp.29-31)
rauh_mc_r_min research Discount rate draw: lower bound 0.015 NPV_dist.R:47; paper eq.24 (S&P 500 dividend yield anchor p.18; eq.19-20 instead specify r-g); NPV_dist_v8.R:22 (NBER chapter p.32, r-g)
rauh_mc_r_max research Discount rate draw: upper bound 0.045 NPV_dist.R:47; paper eq.24; NPV_dist_v8.R:22 (NBER chapter p.32)
nber_mc_f_min guesswork NBER departure fraction of the income tax base: lower bound (drawn independently of WT) 0.3 NPV_dist_v8.R; Jaros and Rauh NBER c15504 Sec 5.4
nber_mc_f_max guesswork NBER departure fraction of the income tax base: upper bound 0.6 NPV_dist_v8.R; Jaros and Rauh NBER c15504 Sec 5.4
q_litigation_survival guesswork Probability the Act survives constitutional challenge (ASC 740-10 more-likely-than-not) 0.5 NBER_2026_litigation_weighted/README.md (assumptions table); not an explicit multiplier in NPV_dist_v8.R - see NOTES.md note 5
nber_growth_rate guesswork on data Growth applied to the Jan-1-2026 snapshot to the Dec-31-2026 valuation date 0.07 final.csv (net_worth_usd_7pct column); NBER README assumptions table
nber_ceiling_hardcoded data wt_max literal in NPV_dist_v8.R (script uses this, not the value re-derived from final.csv) 72 NPV_dist_v8.R:19
nber_ceiling_recomputed derived wt_max re-derived from final.csv: domestic total minus 7 removed_departed rows 72.0515 R/nber_final.R:compute_nber_ceiling() from final.csv
nber_international_net_worth data 28 international billionaires excluded from the domestic base 146.3 final.csv (panel == international, net_worth_usd)
nber_departed_four_grown data Grown taxable wealth of 4 of the 7 publicly reported moves (Page, Thiel, Hankey, Kalanick) 314.823 final.csv (removed_departed rows, wealth_in_tax_base_usd_7pct)
nber_spielberg_sacks_net_worth data Net worth of 2 of the 7 publicly reported moves (Spielberg, Sacks) 9 final.csv (removed_departed rows, net_worth_usd)
wt_lo guesswork Lowest revenue the draw can pick ($B) 0; 35; 45.59 explorer dial (R/site.R)
wt_hi data Highest revenue the draw can pick ($B) 55.10; 67.51; 72 explorer dial (R/site.R)
q guesswork Extra litigation weight on the revenue 1 (none extra); 0.75; 0.50 explorer dial (R/site.R)
c guesswork Billionaires' yearly California income tax ($B) 3.3 to 5.8; 3.3 only; 5.8 only explorer dial (R/site.R)
rate guesswork Discount rate net of growth 1.5% to 4.5%; 1.5% only; 4.5% only explorer dial (R/site.R)
f guesswork Share of the income tax base that leaves tied to revenue; 0.30 to 0.60; 0.283 fixed explorer dial (R/site.R)
Δg guesswork Further growth of the lost income tax 0; 0.5%; 1.0% explorer dial (R/site.R)
R code for the input counts
# The levels list every origin present, so no row falls out of the count.
origin_levels <- union(c("data", "research", "guesswork", "guesswork on data", "scenario", "derived"),
                       unique(odf$origin))
cnt <- as.data.frame(table(factor(odf$origin, levels = origin_levels)))
stopifnot(sum(cnt$Freq) == nrow(odf))
names(cnt) <- c("origin", "rows")
scroll(kable(cnt, caption = paste0("Rows of the table above (", nrow(odf), "), by origin. ",
                                   "The last ", length(dials), " rows are the explorer's dials.")))
Rows of the table above (26), by origin. The last 7 rows are the explorer's dials.
origin rows
data 10
research 3
guesswork 11
guesswork on data 1
scenario 0
derived 1

What reproduces

R code for the output checks
# The chapter's PDF page for each figure it prints (typed, like the printed values), and a
# plain label for each output id (outputs.csv has no description column).
page_nber <- c(revenue_baseline = "19", nber_domestic_grown = "19", nber_ceiling = "21",
               nber_international = "17", pareto_alpha = "26", income_tax_mc_k212 = "30",
               income_tax_mc_k300 = "30", income_tax_mc_k500 = "30", npv_mc_nber_mean = "35",
               npv_mc_nber_median = "35", npv_mc_nber_pct_negative = "35")
what_nber <- c(revenue_baseline = "Baseline revenue, before anyone leaves ($B)",
               nber_domestic_grown = "Baseline grown 7% to the valuation date ($B)",
               nber_ceiling = "Revenue left after the seven publicly reported moves ($B)",
               nber_international = "Net worth of the non-US-citizen residents left out ($B)",
               pareto_alpha = "Pareto tail parameter, alpha",
               income_tax_mc_k212 = "Yearly income tax C, billionaires among the top 212 filers ($B)",
               income_tax_mc_k300 = "Yearly income tax C, billionaires among the top 300 filers ($B)",
               income_tax_mc_k500 = "Yearly income tax C, billionaires among the top 500 filers ($B)",
               npv_mc_nber_mean = "Mean NPV, seeded run ($B)",
               npv_mc_nber_median = "Median NPV, seeded run ($B)",
               npv_mc_nber_pct_negative = "Share of draws below zero (%)")
ob <- outputs[outputs$output_id %in% names(page_nber), ]
top_id <- ob$output_id[which.max(ob$abs_diff)]
stopifnot(top_id == "revenue_baseline")   # the prose below names it as note 6
o <- ob %>% transmute(output = what_nber[output_id], reproduced = signif(value, 6),
  printed = printed_value, `abs diff` = signif(abs_diff, 2), page = page_nber[output_id])
# The rows whose reproduced value does not round to the digits the paper prints.
digits_printed <- vapply(as.character(ob$printed_value), function(p) nchar(sub("^[^.]*[.]?", "", p)), 0)
off_print <- ob$output_id[abs(round(ob$value, digits_printed) - ob$printed_value) > 1e-9]
stopifnot(setequal(off_print, c("revenue_baseline", "nber_ceiling")))   # the prose names these two
# One cat() call: the prose, then the table, so the code shows in one fold.
top_rel <- 100 * max(ob$abs_diff) / ob$printed_value[ob$output_id == top_id]
cat("The code reproduces the chapter's published results (tests in `tests/testthat/`). ",
    "The simulated figures lie within four Monte Carlo standard errors of the print. ",
    "The deterministic figures round to the printed digit, apart from ",
    c("one", "two", "three", "four")[length(off_print)], " where the paper prints a typed or ",
    "rounded intermediate value: the baseline revenue, a typed 94.20 ",
    "([note 6](materials.html#reproduction-notes)), and the ceiling, the difference of two rounded ",
    "figures ([note 4](materials.html#reproduction-notes)). The larger gap is the baseline revenue's, ",
    num(max(ob$abs_diff), 2), " (", num(top_rel, 1), "%). Table 9's 21 cells and Table 10's 3 cells, ",
    "shown above, match as well.^[The first version's own outputs are in the ",
    "[appendix](#appendix-first-version).]\n",
    paste(capture.output(scroll(kable(o, row.names = FALSE))), collapse = "\n"), sep = "")

The code reproduces the chapter’s published results (tests in tests/testthat/). The simulated figures lie within four Monte Carlo standard errors of the print. The deterministic figures round to the printed digit, apart from two where the paper prints a typed or rounded intermediate value: the baseline revenue, a typed 94.20 (note 6), and the ceiling, the difference of two rounded figures (note 4). The larger gap is the baseline revenue’s, 0.10 (0.1%). Table 9’s 21 cells and Table 10’s 3 cells, shown above, match as well.4

output reproduced printed abs diff page
Pareto tail parameter, alpha 1.43789 1.44 0.00210 26
Yearly income tax C, billionaires among the top 212 filers ($B) 5.75702 5.76 0.00300 30
Yearly income tax C, billionaires among the top 300 filers ($B) 4.60902 4.61 0.00098 30
Yearly income tax C, billionaires among the top 500 filers ($B) 3.30815 3.31 0.00180 30
Baseline revenue, before anyone leaves ($B) 94.30430 94.20 0.10000 19
Baseline grown 7% to the valuation date ($B) 100.90600 100.90 0.00560 19
Revenue left after the seven publicly reported moves ($B) 72.05150 72.06 0.00850 21
Net worth of the non-US-citizen residents left out ($B) 146.30000 146.30 0.00000 17
Mean NPV, seeded run ($B) -38.88630 -38.90 0.01400 35
Median NPV, seeded run ($B) -35.25550 -35.30 0.04500 35
Share of draws below zero (%) 85.17200 85.20 0.02800 35
R code for the list of reproduction notes
mm <- readLines("../NOTES.md")
mm_lines <- sub("^## ", "", grep("^## [0-9]+\\.", mm, value = TRUE))
mm_first <- grepl("^[0-9]+\\. First version: ", mm_lines)
# each note's kind, from the "**Kind:**" line under its heading
mm_kind <- sub("^[*][*]Kind:[*][*] ([^.]+)[.].*", "\\1", grep("^[*][*]Kind:[*][*] ", mm, value = TRUE))
stopifnot(length(mm_kind) == length(mm_lines),
          all(mm_kind %in% c("rounded or typed input", "seed variation", "documentation")))
word <- function(n) c("One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine", "Ten")[n]
kinds <- c(`rounded or typed input` = "rounded or typed input", `seed variation` = "seed variation",
           documentation = "documentation note")
by_kind <- vapply(names(kinds), function(k) {
  n <- sum(mm_kind == k)
  paste0(tolower(word(n)), " ", kinds[[k]], if (n > 1) "s" else "")
}, "")
cat(paste0(
  word(length(mm_lines)), " notes record where the authors' text, their READMEs and their code ",
  "describe the same step differently. None is a failure to reproduce a printed result. By kind: ",
  paste(by_kind[-length(by_kind)], collapse = ", "), " and ", by_kind[length(by_kind)], ". ",
  word(sum(!mm_first)), " of them concern the chapter. This OPA records them rather than changing the code.",
  "^[The other ", tolower(word(sum(mm_first))), " concern only the authors' first version: ",
  paste(paste0(sub("First version: ", "", mm_lines[mm_first]), " (", mm_kind[mm_first], ")"), collapse = "; "), ".]\n\n",
  paste0(mm_lines[!mm_first], " (", mm_kind[!mm_first], ")", collapse = "\n"),
  "\n\nFull text: [the materials page](materials.html#reproduction-notes).\n"))

Ten notes record where the authors’ text, their READMEs and their code describe the same step differently. None is a failure to reproduce a printed result. By kind: three rounded or typed inputs, one seed variation and six documentation notes. Five of them concern the chapter. This OPA records them rather than changing the code.5

  1. The sentence after eq. 19 writes C/r where the draw is (r - g) (documentation)
  2. NBER ceiling: one quantity at three roundings (72 in the script, 72.06 in the README and eq. 4, 72.05 in final.csv) (rounded or typed input)
  3. The litigation weight q = 0.50 is carried by the revenue draw, not by a multiplier in the code (documentation)
  4. Baseline revenue: the paper types 94.20 and builds on it; the workbook’s own sum is 94.30 (rounded or typed input)
  5. The top 212’s share of bracket tax is printed as 39.0% and as 38.8% for one formula (documentation)

Full text: the materials page.

Internal identities

These checks compare the pipeline with itself, not with the paper. Each value should be zero or small.

R code for the consistency checks
n_cell <- site_grid_cell(grid, as.list(site_named()$nber))
se_n <- mcn$summary$sd / sqrt(mcn$summary$n)
checks <- data.frame(
  identity = c(
    "Table 10's x* plugged into NPV = WT - x* C/(r - g) gives 0 (max |NPV|, $B)",
    "The explorer's value at the main setting equals the pipeline's analytic mean (abs diff, $B)",
    "Seeded MC mean vs exact expectation, in MC standard errors",
    "Share negative: exact integration (quadrature) vs seeded MC (points)",
    "Ceiling: domestic total minus the 7 departures, recomputed, minus the script's 72 ($B)"),
  value = c(max(abs(with(n10, WT - break_even_pct * C / r))),
    abs(n_cell$mean_npv - tr("npv_mc_nber_analytic_mean")),
    (mcn$summary$mean - tr("npv_mc_nber_analytic_mean")) / se_n,
    n_cell$pct_negative - mcn$summary$pct_negative,
    nc$ceiling_recomputed - nb$wt_max))
checks$value <- vapply(checks$value, function(v) format(signif(v, 3)), "")
scroll(kable(checks))
identity value
Table 10's x* plugged into NPV = WT - x* C/(r - g) gives 0 (max |NPV|, $B) 7.11e-15
The explorer's value at the main setting equals the pipeline's analytic mean (abs diff, $B) 0
Seeded MC mean vs exact expectation, in MC standard errors 0.79
Share negative: exact integration (quadrature) vs seeded MC (points) 0.149
Ceiling: domestic total minus the 7 departures, recomputed, minus the script's 72 ($B) 0.0515

The seeded mean sits within about one Monte Carlo standard error of the exact expectation. The printed −38.9 equals the rounded seeded run, not the rounded expectation, −39.0. Which one a printed digit matches is a rounding coincidence.6

Reproducing this document

cd rjkdc-analysis
export RAUH_REPO_DIR=/path/to/wealth_tax     # clone of github.com/bjaros20/wealth_tax at 25e84dd
Rscript -e 'targets::tar_make()'             # writes the targets store, export/r/, site/data/, site/explorer/grid.js
Rscript tests/testthat.R                     # full suite
quarto render site/repro.qmd

The authors’ repository is MIT licensed but not redistributed here; the tests that re-run the authors’ own scripts skip cleanly without it, and the rest of the suite does not need it.

Appendix: the authors’ first version (March 2026)

This appendix is not part of the account above. It records what the pipeline also reproduces: the authors’ first version, Rauh, Jaros, Kearney, Doran and Cosso, The Net Present Value of the Billionaire Tax Act, Hoover Institution, SSRN 6340778 (17 March 2026). Page numbers in this appendix are that version’s PDF pages. Its Pareto fit and income tax Monte Carlo are the ones in Section 5.1 above (its Table 8 is the chapter’s Table 9). Its revenue part and its Monte Carlo differ.

Revenue. It starts from the same $94.20B baseline, without growth to a valuation date or a litigation discount. Six publicly reported moves leave $67.51B (printed $67.51B, p.11); ten reported departures leave $55.10B (printed $55.1B, p.13). The Swiss semi-elasticity, applied linearly (10.32 × 5 = 51.6% of the base), gives $45.59B (printed $45.59B, p.14). The preferred “approximately $40 billion (range: $35-46 billion)” (p.14) is not computed anywhere in the authors’ code. Every step after the baseline uses a typed 94.2 rather than the workbook’s 94.30, and each printed result built on it reproduces (note 6).

NPV by scenario and break-even (its Tables 9 and 10). The departure share is tied to revenue, \(f = 1 - WT/94.2\), so revenue lost and income tax lost move together.

R code for the first version’s Table 9
p9 <- printed[printed$table == "table9", ]
t9p <- t9 %>% mutate(key = paste0(scenario, "@", r)) %>% rowwise() %>% mutate(
  pv_printed = p9$printed[p9$row == key & p9$column == "pv_lost"],
  npv_printed = p9$printed[p9$row == key & p9$column == "npv"]) %>% ungroup()
tab9 <- t9p %>% transmute(Scenario = label, `WT ($B)` = num(WT, 2), `C ($B)` = num(C, 2),
  `f` = pct(100 * f), `r - g` = pct(100 * r),
  `PV lost` = num(pv_lost), `printed` = num(pv_printed),
  `NPV` = num(npv), `printed ` = num(npv_printed))
m9 <- sum(abs(round(t9p$pv_lost, 1) - t9p$pv_printed) < 1e-9) + sum(abs(round(t9p$npv, 1) - t9p$npv_printed) < 1e-9)
scroll(kable(tab9, align = "lrrrrrrrr", caption = "First version, Table 9, $B (its PDF p.19)"))
First version, Table 9, $B (its PDF p.19)
Scenario WT ($B) C ($B) f r - g PV lost printed NPV printed
Central Scenario 42.00 4.55 55.4% 1.5% 168.1 168.1 −126.1 −126.1
Central Scenario 42.00 4.55 55.4% 3.0% 84.0 84.0 −42.0 −42.0
Central Scenario 42.00 4.55 55.4% 4.5% 56.0 56.0 −14.0 −14.0
Only 6 Confirmed Departures 67.51 3.30 28.3% 1.5% 62.3 62.3 5.2 5.2
Only 6 Confirmed Departures 67.51 3.30 28.3% 3.0% 31.2 31.2 36.3 36.3
Only 6 Confirmed Departures 67.51 3.30 28.3% 4.5% 20.8 20.8 46.7 46.7
Literature Calibrated Departures 35.00 5.80 62.8% 1.5% 243.0 243.0 −208.0 −208.0
Literature Calibrated Departures 35.00 5.80 62.8% 3.0% 121.5 121.5 −86.5 −86.5
Literature Calibrated Departures 35.00 5.80 62.8% 4.5% 81.0 81.0 −46.0 −46.0
R code for the first version’s Table 10
p10 <- printed[printed$table == "table10", ]
tab10 <- t10 %>% mutate(key = paste0(scenario, "@", r)) %>% rowwise() %>%
  mutate(printed = p10$printed[p10$row == key]) %>% ungroup() %>%
  transmute(Scenario = label, `r - g` = pct(100 * r), `x*` = pct(100 * break_even_pct),
            printed = pct(printed))
m10 <- sum(abs(round(100 * t10$break_even_pct, 1) -
  sapply(paste0(t10$scenario, "@", t10$r), function(k) p10$printed[p10$row == k])) < 1e-9)
scroll(kable(tab10, align = "lrrr", caption = "First version, Table 10 (its PDF p.20)"))
First version, Table 10 (its PDF p.20)
Scenario r - g x* printed
Central Scenario 1.5% 13.8% 13.8%
Central Scenario 3.0% 27.7% 27.7%
Central Scenario 4.5% 41.5% 41.5%
Only 6 Confirmed Departures 1.5% 30.7% 30.7%
Only 6 Confirmed Departures 3.0% 61.4% 61.4%
Only 6 Confirmed Departures 4.5% 92.1% 92.1%
Literature Calibrated Departures 1.5% 9.1% 9.1%
Literature Calibrated Departures 3.0% 18.1% 18.1%
Literature Calibrated Departures 4.5% 27.2% 27.2%

18 of the 18 Table 9 cells and 9 of the 9 Table 10 cells match the printed digit. The central scenario’s $42B is a typed literal with no derivation (note 8). The headline is not a cell of Table 9: the central cell at 1.5% is −⁠$126.1B.

Monte Carlo (its Sec 5.5). It draws \(WT \sim U[35, 67.51]\), \(C \sim U[3.3, 5.8]\) and \(r \sim U[0.015, 0.045]\), with \(f = 1 - WT/94.2\). The paper prints the upper bound as 67.1, a typo for 67.51 (note 1). The text derives a growing perpetuity, but the code divides by the plain rate (note 2). The draw’s revenue has a mean of $51.26B, above the preferred $40B.

R code for the first version’s Monte Carlo
s1 <- tr("npv_mc_ssrn_summary")
scroll(kable(data.frame(
  ` ` = c("reproduced (seed 2026)", "printed (its PDF p.20)"),
  `Mean` = c(num(s1$mean, 2), num(h("ssrn", "mc_mean_npv", "printed"))),
  `Median` = c(num(s1$median, 2), num(h("ssrn", "mc_median_npv", "printed"))),
  `SD` = c(num(s1$sd, 2), num(h("ssrn", "mc_sd_npv", "printed"))),
  `% negative` = c(num(s1$pct_negative, 2), num(h("ssrn", "mc_pct_negative", "printed"))),
  check.names = FALSE), align = "lrrrr",
  caption = "First version, NPV Monte Carlo, $B, seed 2026, 100,000 draws"))
First version, NPV Monte Carlo, $B, seed 2026, 100,000 draws
Mean Median SD % negative
reproduced (seed 2026) −24.77 −19.10 38.60 71.02
printed (its PDF p.20) −24.7 −19.1 38.4 71.0

This OPA’s R port agrees with an unmodified run of NPV_dist.R to machine precision. The median and share negative round to the printed values. The mean (−24.77 against −24.7) and the standard deviation (38.60 against 38.4) lie within Monte Carlo error of the print, so the summary reproduces; the small gaps are seed variation (note 9). The draws are independent, so the mean has a closed form:

\[E[NPV] = E[WT] - \left(1 - \frac{E[WT]}{94.20}\right) E[C]\, E\!\left[\tfrac{1}{r}\right]\]

It equals −⁠$24.707B, which rounds to the printed −24.7. The SD has a closed form too (npv_mc_analytic_sd()), 38.423, which rounds to the printed 38.4. So the printed mean and SD are the model’s exact values at the printed digit, and the printed summary is what the same code gives on another run. The shipped seed gives −24.77 and 38.60.

Its outputs, inputs and checks.

R code for the first version’s outputs, inputs and checks
of <- outputs[!outputs$output_id %in% names(page_nber) & outputs$version != "nber", ]
scroll(kable(of %>% transmute(output = output_id, reproduced = signif(value, 6),
  printed = printed_value, `abs diff` = signif(abs_diff, 2), `its page` = printed_page),
  row.names = FALSE, caption = "First version: outputs"))
First version: outputs
output reproduced printed abs diff its page
revenue_confirmed6 67.5106 67.51 0.00065 11
revenue_expanded10 55.0964 55.10 0.00360 13
revenue_literature_calibrated 45.5928 45.59 0.00280 14
table9_central_npv_r015 -126.0890 -126.10 0.01100 19
npv_mc_ssrn_mean -24.7681 -24.70 0.06800 20
npv_mc_ssrn_median -19.1004 -19.10 0.00045 20
npv_mc_ssrn_sd 38.5955 38.40 0.20000 20
npv_mc_ssrn_pct_negative 71.0240 71.00 0.02400 20
R code for the first version’s outputs, inputs and checks
scroll(kable(inp_row(inputs[first_only, ]), row.names = FALSE, caption = "First version: inputs"))
First version: inputs
input origin what value(s) where
brulhart_semi_elasticity research Migration semi-elasticity, Brulhart et al. (2022) 10.32 Summary_Preferred!I8 (paper eq.12)
wt_central_scenario scenario Table 9 Central Scenario wealth tax revenue (literal input, not linked to Sec 4's chain) 42 NPV_calculations_5.2.xlsx!B12
rauh_mc_wt_min guesswork Monte Carlo wealth tax floor (literature-calibrated scenario; semi-elasticity about 12.6) 35 NPV_dist.R (wt ~ U[35, 67.51]); paper eq.22 and Table 9
R code for the first version’s outputs, inputs and checks
x10 <- t10 %>% left_join(t9 %>% select(scenario, r, WT, C), by = c("scenario", "r", "WT", "C"))
s_cell <- site_grid_cell(grid, as.list(site_named()$ssrn))
se_s <- mcs$summary$sd / sqrt(mcs$summary$n)
checks1 <- data.frame(
  identity = c(
    "Table 10's x* plugged into Table 9's formula gives NPV = 0 (max |NPV|, $B)",
    "Grid's first-version cell equals the pipeline's analytic mean (abs diff, $B)",
    "Seeded MC mean vs exact expectation, in MC standard errors",
    "Share negative: quadrature vs seeded MC (points)"),
  value = c(max(abs(with(x10, WT - break_even_pct * C / r))),
    abs(s_cell$mean_npv - tr("npv_mc_ssrn_analytic_mean")),
    (mcs$summary$mean - tr("npv_mc_ssrn_analytic_mean")) / se_s,
    s_cell$pct_negative - mcs$summary$pct_negative))
checks1$value <- vapply(checks1$value, function(v) format(signif(v, 3)), "")
scroll(kable(checks1, caption = "First version: internal identities"))
First version: internal identities
identity value
Table 10's x* plugged into Table 9's formula gives NPV = 0 (max |NPV|, $B) 7.11e-15
Grid's first-version cell equals the pipeline's analytic mean (abs diff, $B) 0
Seeded MC mean vs exact expectation, in MC standard errors -0.501
Share negative: quadrature vs seeded MC (points) 0.00385

Footnotes

  1. The authors released a first version in March 2026: Rauh, Jaros, Kearney, Doran and Cosso, Hoover Institution, SSRN 6340778. It printed revenue of about $40B and a mean NPV of −⁠$24.7B, with 71% of outcomes negative (its PDF p.1). The pipeline reproduces it too; the appendix at the end of this report sets out what it printed and which checks concern it.↩︎

  2. The pipeline’s input file (export/r/inputs.csv) labels the range research, because of its published anchor. The comparison, which uses one rate worth the same as the draw, labels that rate derived (disputes row 10). This report labels the range guesswork, because the width of the band moves the result.↩︎

  3. The pipeline’s input file also lists 3 rows sourced to the authors’ first version (March 2026): its revenue floor, its central scenario and the Swiss semi-elasticity as its workbook applies it. The appendix shows them.↩︎

  4. The first version’s own outputs are in the appendix.↩︎

  5. The other five concern only the authors’ first version: 1. eq. 22 prints the revenue ceiling as 67.1; the code and the rest of the text use 67.51 (documentation); 2. the text derives a growing perpetuity (r-g); the Monte Carlo code divides by plain r (documentation); 7. two memo cells, Summary_Expanded!D23:D24, read from the wrong sheet (stale copy-paste) (documentation); 8. the Table 9 central scenario (WT = $42.0B) is a typed input with no derivation (rounded or typed input); 9. the printed Monte Carlo summary comes from a different run of the same code (seed variation).↩︎

  6. In the authors’ first version it is the other way round: its printed mean equals the rounded expectation (see the appendix).↩︎