A friend messaged me a photo of a sealed booster box last month with one question: "worth it?" He'd already decided, really. The chase card in that set was all over his feed, so the box felt like a good deal. I asked him to send me the pull rates instead of the hype, and we spent twenty minutes turning "worth it?" into something we could actually compute.
That exercise is a small, self-contained data problem. It's also a good example of how a clean-looking model can hand you a confident number that doesn't survive contact with reality. If you like building little estimators, this one is worth doing carefully, because the interesting part isn't the formula. It's everything the formula assumes.
The formula is the easy part
Expected value of a box is a weighted sum. Each card you can pull has a probability and a market value, and you multiply the two across every slot the box gives you. That's it. Undergrad probability.
Here's a stripped-down version for a hypothetical set. I'm using made-up numbers so nobody mistakes this for real pull data — the point is the shape of the computation, not the specific set.
# One "hit slot" in a box: probabilities cover the full outcome space.
# Values are illustrative market estimates in USD.
hit_table = [
{"name": "Alt-art chase", "p": 0.0125, "value": 180.00},
{"name": "Secret rare", "p": 0.030, "value": 45.00},
{"name": "Full-art rare", "p": 0.100, "value": 8.00},
{"name": "Standard hit", "p": 0.400, "value": 0.55},
{"name": "No notable hit", "p": 0.4575, "value": 0.06},
]
assert abs(sum(row["p"] for row in hit_table) - 1.0) < 1e-9
ev_per_slot = sum(row["p"] * row["value"] for row in hit_table)
hit_slots_per_box = 36 # e.g. one meaningful slot per pack
ev_box = ev_per_slot * hit_slots_per_box
print(f"EV per slot: ${ev_per_slot:.2f}") # $4.65
print(f"EV per box: ${ev_box:.2f}") # $167.31
The box costs $150 sealed, the model says $167, so you're "up." My friend was thrilled for about four seconds, which is roughly how long that number deserves.
Where it starts lying
The math is correct. The inputs are where it falls apart, and they fall apart in ways that all push the same direction: optimism.
The distribution is heavy-tailed, so the mean is a bad summary. That $167 leans hard on the 1.25% alt-art chase, which alone contributes $2.25 of the $4.65 per-slot figure. Drop that single row and EV per box falls to about $86 — far below the sticker price. Almost half of the "expected" value lives in an outcome that lands in roughly one box out of eighty per slot. The typical buyer does much worse than the mean buyer, and the word "expected" hides that completely.
# Probability a whole box shows zero copies of the chase card
p_miss_chase = (1 - 0.0125) ** hit_slots_per_box
print(f"Chance of no chase in a box: {p_miss_chase:.1%}") # 63.6%
Roughly two out of three boxes never see the card carrying most of the EV. A mean that ignores that is technically true and practically misleading.
The values decay while you're holding them. value isn't a constant, it's a snapshot. Singles prices for a freshly released set slide as supply floods in, and the chase you priced at $180 on release week is a different number a month later. A model built on today's prices silently assumes you can liquidate instantly at today's prices. You can't.
The probabilities are the shakiest input of all. Very few sets publish official per-slot pull rates. What circulates is a mix of manufacturer statements, community-aggregated opening logs, and outright guesses, and they rarely agree on the long tail — which, per the point above, is exactly the part that moves your answer most. Pinning down a defensible probability for that 1.25% row is harder than the entire rest of the calculation.
That last problem is the one I actually spend time on. When I need a starting distribution I'll pull from a set-odds reference like TCGOdds to see what pull-rate and set-composition data is available for a release, then treat those figures as a documented estimate to sanity-check against rather than as ground truth. It doesn't remove the uncertainty in the tail — nothing does — but it beats a screenshot of one person's box break, and it gives you a consistent baseline to plug into the model above instead of a number you felt was about right.
Turn the point estimate into a range
Because the inputs are uncertain, a single EV figure is the wrong output type. A quick Monte Carlo makes the spread visible, which is the honest version of the answer.
import random
def simulate_box(trials=200_000):
outcomes = []
for _ in range(trials):
total = 0.0
for _ in range(hit_slots_per_box):
r = random.random()
cum = 0.0
for row in hit_table:
cum += row["p"]
if r <= cum:
total += row["value"]
break
outcomes.append(total)
outcomes.sort()
return outcomes
box_values = simulate_box()
n = len(box_values)
pct = lambda q: box_values[int(q * n)]
print(f"mean ${sum(box_values)/n:.0f} | "
f"p10 ${pct(0.10):.0f} | p50 ${pct(0.50):.0f} | p90 ${pct(0.90):.0f}")
Now the answer isn't "$167." It's a shape: the mean sits near $167, but the median box lands in the low $80s, a bottom tenth comes in under $50, and the top tenth clears $270 on the back of a chase pull. Against a $150 box, that reframes the decision entirely. You're not buying an expected $167. You're buying a wide distribution where the good outcomes are rare and the typical one is underwater before fees, shipping, and the price decay you haven't modeled yet.
What the model is actually for
None of this tells you whether to buy the box. Expected value isn't profit, and it was never meant to be a purchase recommendation. For something you buy partly for the fun of opening it, the entertainment is a real part of the return and it doesn't show up anywhere in these numbers. Pull rates and market values are estimates, and a model built on estimates gives you a planning aid, not a promise. Treat any figure it produces as "here's the shape of the outcomes," not "here's what you'll get."
The value of writing the estimator is that it makes your assumptions explicit. You can see, in code, that your optimism was concentrated in one 1.25% row and one stale price. That's a better position than "it felt worth it," even when the conclusion is identical.
My friend bought the box. He'd run the numbers, he knew the median outcome, and he wanted to open it anyway. Completely defensible. It's just a different decision than the one he was about to make twenty minutes earlier, and the difference was a dozen lines of Python and a distribution he could finally look at.
Top comments (0)