Is your ChatGPT ads test profitable? A small Python model with a sensitivity table
Before launching a paid test on a new ad channel, small teams usually ask "how much should we spend?". The more useful question is "at what cost per click does this stop making money?". This post builds a dependency-free Python model that answers both, then adds a sensitivity table so you can see which input actually moves the result.
The model
from dataclasses import dataclass, replace
@dataclass(frozen=True)
class Plan:
monthly_budget: float
avg_cpc: float
cvr: float # conversion rate, 0.03 = 3 %
value: float # revenue per conversion
margin: float # gross margin, 0.40 = 40 %
goal: int = 0 # conversions wanted per month
peak_factor: float = 2.0 # ASSUMPTION: worst single day vs average day
@property
def clicks(self): return self.monthly_budget / self.avg_cpc
@property
def conversions(self): return self.clicks * self.cvr
@property
def cpa(self): return self.monthly_budget / self.conversions
@property
def break_even_cpc(self): return self.value * self.margin * self.cvr
@property
def profit(self): return self.conversions * self.value * self.margin - self.monthly_budget
def report(self):
daily = self.monthly_budget / (365 / 12)
return {
"daily_budget": round(daily, 2),
"clicks": round(self.clicks),
"conversions": round(self.conversions, 1),
"cpa": round(self.cpa, 2),
"roas": round(self.conversions * self.value / self.monthly_budget, 2),
"gross_profit": round(self.profit, 2),
"break_even_cpc": round(self.break_even_cpc, 2),
"budget_for_goal": round(self.goal * self.cpa, 2),
"planning_peak_day": round(daily * self.peak_factor, 2),
}
base = Plan(900, 2.50, 0.03, 250, 0.40, goal=20)
print(base.report())
{'daily_budget': 29.59, 'clicks': 360, 'conversions': 10.8, 'cpa': 83.33, 'roas': 3.0, 'gross_profit': 180.0, 'break_even_cpc': 3.0, 'budget_for_goal': 1666.67, 'planning_peak_day': 59.18}
A note on peak_factor: ad platforms often let a single day spend above the average daily budget, and the allowed amount differs by platform. The 2.0 here is our planning assumption, not a documented rule for any specific ad product. Set it from your platform's own help pages, or leave it at 2 and use it as a cash-flow buffer. Pacing behaviour is covered in more depth in our budget pacing guide.
Reading the output
- Break-even CPC = value × margin × CVR. At €250 per sale, a 40% margin and 3% conversion, any click above €3.00 loses money. That single number is more useful than a budget target, because it tells you what to do when the auction gets expensive.
- A 3× ROAS can still barely profit. At 40% margin, 3× ROAS leaves €180 a month. ROAS without margin is a vanity metric; the break-even ROAS guide shows the same idea from the other direction (break-even ROAS = 1 ÷ margin, so 2.5× here).
- 10.8 conversions a month is about 2.5 a week. That's too few to judge a test in days. Plan it in weeks, and decide in advance how many conversions you need before you'll call it.
The sensitivity table
The model is linear in some inputs and not in others, so it's worth seeing them side by side. This varies each input by ±20% while holding the others at the base values:
def sensitivity(plan, fields=("avg_cpc", "cvr", "value", "margin"), step=0.2):
rows = []
for f in fields:
for sign in (-1, 1):
p = replace(plan, **{f: getattr(plan, f) * (1 + sign * step)})
rows.append((f, f"{sign*step:+.0%}", round(p.profit, 0), round(p.break_even_cpc, 2)))
return rows
print(f"{'input':<8}{'change':>7}{'profit €':>10}{'BE CPC €':>10}")
for r in sensitivity(base):
print(f"{r[0]:<8}{r[1]:>7}{r[2]:>10}{r[3]:>10}")
input change profit € BE CPC €
avg_cpc -20% 450.0 3.0
avg_cpc +20% 0.0 3.0
cvr -20% -36.0 2.4
cvr +20% 396.0 3.6
value -20% -36.0 2.4
value +20% 396.0 3.6
margin -20% -36.0 2.4
margin +20% 396.0 3.6
Three things stand out:
- A 20% rise in CPC wipes out the whole profit. €2.50 + 20% is exactly the €3.00 break-even CPC, so the test has no headroom on price.
- CVR, value and margin act identically. They multiply together in both profit and break-even CPC, so a 20% gain in any one is worth the same. In practice CVR is usually the cheapest to move: it depends on your page and offer, not on the auction.
- Break-even CPC doesn't depend on CPC. Obvious once written down, but it's why break-even CPC makes a good bid ceiling: it stays fixed while the market moves.
Extending it
- Add a
weeks_to_n(n)helper,n / (self.conversions / (365/12/7)), to estimate how long a test takes to reach a given number of conversions. - Run
sensitivitywithstep=0.5to stress-test a launch plan. - Replace the single
avg_cpcwith a list of daily CPCs from your first week and recompute everything from real data.
The point is not precision. Writing the economics down as code makes the assumptions visible, including the ones that are only assumptions, like peak_factor.
Comments
Join the discussionNo comments yet. Be the first to comment.