<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Athilly Ads Notes]]></title><description><![CDATA[Athilly Ads Notes]]></description><link>https://athillyads.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Sat, 31 Oct 2026 16:38:24 GMT</lastBuildDate><atom:link href="https://athillyads.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Is your ChatGPT ads test profitable? A small Python model with a sensitivity table]]></title><description><![CDATA[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…]]></description><link>https://athillyads.hashnode.dev/is-your-chatgpt-ads-test-profitable-a-small-python-model-with-a-sensitivity-table</link><guid isPermaLink="true">https://athillyads.hashnode.dev/is-your-chatgpt-ads-test-profitable-a-small-python-model-with-a-sensitivity-table</guid><category><![CDATA[Python]]></category><category><![CDATA[marketing]]></category><category><![CDATA[analytics]]></category><category><![CDATA[Advertising]]></category><dc:creator><![CDATA[Athilly Ads]]></dc:creator><pubDate>Fri, 23 Oct 2026 03:07:45 GMT</pubDate><content:encoded><![CDATA[<p>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.</p>
<h2>The model</h2>
<pre><code class="language-python">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())
</code></pre>
<pre><code>{'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}
</code></pre>
<p>A note on <code>peak_factor</code>: 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 <strong>our planning assumption, not a documented rule</strong> 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 <a href="https://athillyads.com/guides/chatgpt-ads-budget-pacing/">budget pacing guide</a>.</p>
<h2>Reading the output</h2>
<ul>
<li><strong>Break-even CPC = value × margin × CVR.</strong> 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.</li>
<li><strong>A 3× ROAS can still barely profit.</strong> At 40% margin, 3× ROAS leaves €180 a month. ROAS without margin is a vanity metric; the <a href="https://athillyads.com/guides/chatgpt-ads-break-even-roas/">break-even ROAS guide</a> shows the same idea from the other direction (break-even ROAS = 1 ÷ margin, so 2.5× here).</li>
<li><strong>10.8 conversions a month is about 2.5 a week.</strong> 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.</li>
</ul>
<h2>The sensitivity table</h2>
<p>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:</p>
<pre><code class="language-python">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':&lt;8}{'change':&gt;7}{'profit €':&gt;10}{'BE CPC €':&gt;10}")
for r in sensitivity(base):
    print(f"{r[0]:&lt;8}{r[1]:&gt;7}{r[2]:&gt;10}{r[3]:&gt;10}")
</code></pre>
<pre><code>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
</code></pre>
<p>Three things stand out:</p>
<ol>
<li><strong>A 20% rise in CPC wipes out the whole profit.</strong> €2.50 + 20% is exactly the €3.00 break-even CPC, so the test has no headroom on price.</li>
<li><strong>CVR, value and margin act identically.</strong> 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.</li>
<li><strong>Break-even CPC doesn't depend on CPC.</strong> Obvious once written down, but it's why break-even CPC makes a good bid ceiling: it stays fixed while the market moves.</li>
</ol>
<h2>Extending it</h2>
<ul>
<li>Add a <code>weeks_to_n(n)</code> helper, <code>n / (self.conversions / (365/12/7))</code>, to estimate how long a test takes to reach a given number of conversions.</li>
<li>Run <code>sensitivity</code> with <code>step=0.5</code> to stress-test a launch plan.</li>
<li>Replace the single <code>avg_cpc</code> with a list of daily CPCs from your first week and recompute everything from real data.</li>
</ul>
<p>The point is not precision. Writing the economics down as code makes the assumptions visible, including the ones that are only assumptions, like <code>peak_factor</code>.</p>
]]></content:encoded></item></channel></rss>