Arron Bennett, Founder of Bennett Financials

Most small software agencies change prices the same way: someone gets nervous or brave, edits the rate card, and then reads tea leaves from the next two deals. If both close, the new price "works". If one doesn't, it gets rolled back.

Two deals tell you nothing. Twenty start to tell you something. This post is a tiny Python tracker that compares your old and new price on the numbers that matter, close rate and revenue per call, and refuses to give a verdict until you have enough calls.

Key takeaways

  • Don't overhaul pricing overnight. Raise it for the next 20 qualified calls and measure.
  • Track close rate and revenue per call side by side. A lower close rate can still mean more revenue.
  • A close rate settling in the 30% to 40% range usually means the price is about right. Higher means there's still room.
  • The code is about 50 lines of standard-library Python. No dependencies.

Why test a price on 20 calls?

Testing a price on 20 calls gives you enough conversations for the close rate to mean something, without betting the whole business on a guess. With 3 or 4 calls, one unusual prospect swings the result. Around 20 qualified calls, patterns start to hold, and you can compare the new price against the old one with some confidence.

Here's the short version of the method in my own words:

Raise it for the next 20 prospects, watch the close rate, and let the data confirm. If close rate drops into the 30% to 40% range, you've found the price. If it barely moves, keep walking it up.

The example

Here's a hypothetical example. Every number is made up to show the method.

An agency with 8 engineers does $1.4 million a year building custom web apps. Its last 10 qualified discovery calls at the old price closed 7 times, a 70% close rate, at an average project of about $49,000. That close rate is a hint the price is low, so the founder quotes the next 20 calls at roughly 1.8 times the old average.

Where the data comes from

You probably have most of it already. Your CRM or calendar knows which discovery calls happened. Your proposals or contracts know who signed and for how much. The one field you'll need to add is which price was quoted on each call, and the easiest way is a simple tag on the deal: old or new.

Decide your rules before the first new-price call, and write them down. What counts as a qualified call? Who decides? When does a deal count as lost: a written no, or 30 days of silence? If you set those rules after you see the results, you'll bend them toward the answer you want. Everyone does.

The data

One row per qualified call that actually happened. No-shows and obvious non-fits don't count, because they'd distort the close rate either way. price is old or new, won is 1 or 0, and value is the signed contract value.

In a real setup you'd export this from your CRM as a CSV. For a runnable example it's inline.

The code

import csv import io # One row per qualified discovery call that actually happened. # price: "old" or "new"; won: 1 if they signed; value: signed contract value CALLS = """date,price,won,value 2026-08-04,old,1,48000 2026-08-06,old,0,0 2026-08-11,old,1,52000 2026-08-13,old,1,45000 2026-08-18,old,0,0 2026-08-20,old,1,50000 2026-08-25,old,1,47000 2026-08-27,old,0,0 2026-09-01,old,1,49000 2026-09-03,old,1,51000 2026-09-08,new,0,0 2026-09-10,new,1,88000 2026-09-15,new,0,0 2026-09-17,new,0,0 2026-09-22,new,1,86000 2026-09-24,new,1,91000 2026-09-29,new,0,0 2026-10-01,new,0,0 2026-10-06,new,1,87000 2026-10-08,new,0,0 2026-10-13,new,1,90000 2026-10-15,new,0,0 2026-10-20,new,0,0 2026-10-22,new,1,89000 2026-10-27,new,0,0 2026-10-29,new,1,92000 2026-11-03,new,0,0 2026-11-05,new,1,85000 2026-11-09,new,0,0 2026-11-10,new,0,0 """ MIN_CALLS = 20 def summarise(rows): calls = len(rows) wins = sum(int(r["won"]) for r in rows) revenue = sum(float(r["value"]) for r in rows) return { "calls": calls, "wins": wins, "close_rate": wins / calls if calls else 0.0, "revenue_per_call": revenue / calls if calls else 0.0, } def verdict(new): if new["calls"] < MIN_CALLS: return f"Keep going: {new['calls']} of {MIN_CALLS} calls at the new price." rate = new["close_rate"] if rate > 0.40: return "Close rate still above 40%: the price has more room. Step it up again." if rate >= 0.30: return "Close rate in the 30-40% zone: this price is about right." return "Close rate under 30%: check fit and offer before blaming price." rows = list(csv.DictReader(io.StringIO(CALLS))) old = summarise([r for r in rows if r["price"] == "old"]) new = summarise([r for r in rows if r["price"] == "new"]) for name, s in (("old", old), ("new", new)): print(f"{name:>3}: {s['calls']:>2} calls, {s['wins']:>2} won, " f"close {s['close_rate']:.0%}, revenue per call ${s['revenue_per_call']:,.0f}") print(verdict(new)) 

Output:

old: 10 calls, 7 won, close 70%, revenue per call $34,200 new: 20 calls, 8 won, close 40%, revenue per call $35,400 Close rate in the 30-40% zone: this price is about right. 

Reading the result

The close rate fell from 70% to 40%. If you only watched wins, that looks like failure. But revenue per call went up, from about $34,200 to about $35,400, and the agency won 8 projects out of 20 calls instead of the 14 it might have won at the old price. That's roughly the same revenue for fewer projects, which frees engineers to take on more work or simply ship better.

A few things worth knowing about the numbers:

  • Revenue per call is total signed value divided by calls held. It's the fairest single comparison between two prices because it captures both close rate and deal size.
  • The 20-call minimum is in MIN_CALLS. The script won't give a verdict before then. That's deliberate.
  • The thresholds (40% and 30%) are the bands I use with service businesses. Change them if your market is different, but write down why.

Things that will fool you

Price tests go wrong when something else changes at the same time. Keep these steady during the test:

  1. Lead source. If a big referral partner sends you warm leads mid-test, your close rate will jump for reasons that have nothing to do with price.
  2. Who's selling. A new salesperson changes the result.
  3. Scope. If the new price comes with a different offer, you're testing two things.

And don't mix in no-shows. A no-show isn't a lost deal; it never got to the price.

Extending it

Some easy next steps if you want more from the script:

  • Add a source column and summarise by lead source, so you can see whether referrals and paid leads react differently to the new price.
  • Add a stage column to see where new-price deals drop out: after the proposal, or on the first call.
  • Write the summary to a CSV each month so you can watch the trend.

Keep it small. The point is a decision, not a dashboard.

FAQ

How many calls do I need to test a price increase?

Around 20 qualified calls at the new price is a sensible minimum for a small team. Fewer than that and a couple of unusual deals can swing the result.

Should I compare close rate or revenue?

Both. Close rate tells you how the market is reacting. Revenue per call tells you whether the business is better off.

What if my close rate drops below 30%?

Before cutting price, check fit and offer. Were the prospects the right size and stage? Did they understand what they were buying? If both hold, step the price back a little.

For more background on the close-rate bands used here, see the 50-60% close rate band and the 1.5x raise and Bennett Financials' pricing research.