Build a margin meter before you price your AI feature
A 60-line Python script that prices one real API call against the published per-token rate and prints the gross margin of the feature it powers.
What you'll have running
Sixty lines of Python, one real API call, and the token counts the provider hands back on it. The script prices those against the published per-million-token rate and prints the gross margin of the feature at whatever you charge for it. It also prints break-even calls per seat per month. Most people guess that number. Almost nobody measures it.
Twenty minutes, virtualenv included.
That is the honest answer to "how do AI companies make money" at the operator level: they charge for a unit of work, inference costs them less than that unit, and the business is the gap plus how predictable the gap is across accounts. This script measures your gap.
Prerequisites:
- Python 3.9 or newer, and
pip. - An OpenAI API key from platform.openai.com/api-keys, on an account with credit. One small chat completion per run.
- A terminal.
jqappears in the last step and nowhere else. - Two numbers you believe: what one account pays you per month, and roughly how many times that account hits this feature per month. Guess on the first run; fix it on the second.
The four lines the money arrives on
Every AI revenue model in production is one of these four, or a stack of them. The right-hand column is the thing you have to measure to know if the line is profitable.
| Line | Documented example | What you must measure |
|---|---|---|
| Per-seat subscription | ChatGPT Plus at $20/month; GitHub Copilot's per-user developer plans | Calls per seat per month, and the spread between your heaviest and lightest seat |
| Per-token API resale | OpenAI's published API rates — gpt-4o-mini at $0.150 per 1M input tokens and $0.600 per 1M output tokens | Your markup over the provider rate, after cached and retried tokens |
| Metered per unit of work | Usage-based billing with meters and meter events, as in Stripe's billing docs | Cost of one unit end to end, including the calls the user never sees |
| Compute underneath | The GPUs and clouds the three lines above run on | Not your problem unless you own it; it is the floor under your input cost |
Two published mechanics move the gap more than anything you can negotiate at your volume. Cached input tokens: OpenAI reports them inside prompt_tokens and bills them at a reduced rate, and Anthropic documents cache reads at 0.1x base input price with cache writes at 1.25x. Batch submission: OpenAI's Batch API is discounted against the synchronous endpoint. If your workload has a long stable prefix, caching is not an optimisation to schedule for next quarter — it is the difference between a thin line and a fat one, and shipping without it means you priced against a cost you never had to pay. Batch is narrower than it looks: it trades latency for price, so it only helps work a user is not sitting and waiting on — overnight re-summarisation, backfills, eval sweeps. Interactive requests stay on the synchronous endpoint at the full rate, which means most products end up carrying two cost profiles at once and should measure them separately rather than averaging them into one blended number.
Build the margin meter
- Make a project directory and a virtualenv:
mkdir margin-meter && cd margin-meter && python3 -m venv .venv && source .venv/bin/activate. Your prompt should now be prefixed with(.venv). - Install the client:
pip install openai. The last line of output starts withSuccessfully installed openai-followed by the version it resolved. - Create a key at platform.openai.com/api-keys, then export it in this shell:
export OPENAI_API_KEY="sk-...". Confirm Python can see it without printing the whole thing:python -c "import os; print(os.environ['OPENAI_API_KEY'][:3])"printssk-. - Save the script below as
unit_margin.py. On the first pass you edit two lines:PRICE_PER_SEAT_MONTHandCALLS_PER_SEAT_MONTH. - Run it:
python unit_margin.py "Summarise the state of this support ticket in two sentences.". You get a JSON object on stdout with token counts, a cost in dollars, and a gross margin percentage. - Replace the prompt with the real one your feature sends, system prompt and retrieved context included. Token counts move by an order of magnitude when you paste in actual context. That is the whole point of running this against production shape instead of a toy string.
- Build a fixture file of 20 real-shaped inputs, one per line, identifiers scrubbed to documentation domains —
billing@example.com,acct-4417.example.net. Then take the worst case:for i in $(seq 1 20); do python unit_margin.py "$(sed -n "${i}p" fixtures.txt)"; done | jq -s 'map(.cost_usd) | max'. One number comes back. That number, not the mean, is what you price against. - Put
cost_usdon every request in your logs, keyed by account. Once you can group it, you have the account-level spread the diagram asks about, and you can pick a billing line instead of copying someone else's.
#!/usr/bin/env python3
"""
unit_margin.py - price one real API call and print the gross margin
of the feature it powers.
Usage: OPENAI_API_KEY=sk-... python unit_margin.py "your real prompt"
"""
import json
import sys
from openai import OpenAI
MODEL = "gpt-4o-mini"
# USD per 1,000,000 tokens, copied from https://platform.openai.com/docs/pricing
# This dict is the only thing in the file you have to maintain. Re-check it
# whenever you change models or the vendor changes a published rate.
PRICES = {
"gpt-4o-mini": {"input": 0.150, "cached_input": 0.075, "output": 0.600},
"gpt-4o": {"input": 2.500, "cached_input": 1.250, "output": 10.000},
}
# Your side of the equation.
PRICE_PER_SEAT_MONTH = 49.00 # what one account pays you
CALLS_PER_SEAT_MONTH = 400 # how often that account hits this feature
def price_call(model, usage):
"""Turn a usage object into dollars."""
rates = PRICES[model]
u = usage.model_dump() if hasattr(usage, "model_dump") else dict(usage)
prompt_tokens = u.get("prompt_tokens", 0) or 0
output_tokens = u.get("completion_tokens", 0) or 0
# Cached tokens are reported *inside* prompt_tokens, so subtract them
# before applying the full input rate or you overstate your own cost.
details = u.get("prompt_tokens_details") or {}
cached_tokens = details.get("cached_tokens", 0) or 0
fresh_tokens = max(prompt_tokens - cached_tokens, 0)
cost = (
fresh_tokens / 1_000_000 * rates["input"]
+ cached_tokens / 1_000_000 * rates["cached_input"]
+ output_tokens / 1_000_000 * rates["output"]
)
return {
"fresh_input_tokens": fresh_tokens,
"cached_input_tokens": cached_tokens,
"output_tokens": output_tokens,
"cost_usd": round(cost, 8),
}
def main():
prompt = sys.argv[1] if len(sys.argv) > 1 else "Say hello in one sentence."
client = OpenAI() # reads OPENAI_API_KEY from the environment
resp = client.chat.completions.create(
model=MODEL,
messages=[
{"role": "system", "content": "You are a terse support triage assistant."},
{"role": "user", "content": prompt},
],
)
call = price_call(MODEL, resp.usage)
per_seat = call["cost_usd"] * CALLS_PER_SEAT_MONTH
margin = (PRICE_PER_SEAT_MONTH - per_seat) / PRICE_PER_SEAT_MONTH
print(json.dumps({
"model": MODEL,
**call,
"cost_per_1k_calls_usd": round(call["cost_usd"] * 1000, 4),
"cost_per_seat_month_usd": round(per_seat, 4),
"price_per_seat_month_usd": PRICE_PER_SEAT_MONTH,
"gross_margin_pct": round(margin * 100, 2),
"breakeven_calls_per_seat": (
int(PRICE_PER_SEAT_MONTH // call["cost_usd"]) if call["cost_usd"] else None
),
}, indent=2))
if __name__ == "__main__":
main()
Your token counts will differ — the model decides how long to answer. The shape of a successful run is this:
{
"model": "gpt-4o-mini",
"fresh_input_tokens": 1240,
"cached_input_tokens": 0,
"output_tokens": 310,
"cost_usd": 0.000372,
"cost_per_1k_calls_usd": 0.372,
"cost_per_seat_month_usd": 0.1488,
"price_per_seat_month_usd": 49.0,
"gross_margin_pct": 99.7,
"breakeven_calls_per_seat": 131720
}
Read breakeven_calls_per_seat first. At those rates a $49 seat covers 131,720 calls of this exact shape before the feature stops paying for itself — which is why a single-call feature on a small model looks like software margin and gets priced like software. Nothing about that survives contact with an agent loop.
The same code inside an agent loop
Change nothing but the call pattern and the arithmetic flips. Take an agent that runs 40 turns, carries roughly 15,000 input tokens of context per turn because it re-sends the transcript, and emits 1,500 output tokens per turn. Against the published gpt-4o rates:
| Component | Tokens | Rate | Cost |
|---|---|---|---|
| Input | 40 × 15,000 = 600,000 | $2.50 / 1M | $1.50 |
| Output | 40 × 1,500 = 60,000 | $10.00 / 1M | $0.60 |
| One run | $2.10 |
That is arithmetic on published prices, not a measurement of your workload. Run that agent 5,000 times in a month and the line item is $10,500. A $49 seat now covers 23 runs. The feature is not software any more; it is a service with a cost of goods, and you either meter it, cap it, or price it per run.
Here the failure modes stop being financial and start being engineering. Unbounded loops are the most common production failure in agent systems: a model that cannot complete a task will keep retrying variations of the same step, and every retry bills. The fix is a step cap enforced outside the model — a counter in your own code that hard-stops the loop. An instruction in the prompt asking it to stop is not a cap; it is a suggestion to the thing that is already failing to reason. Same story for tool calls that silently re-send a growing transcript. Measure the transcript, not the last message. I wrote more about keeping the boundary tight in Be more agentic by building one bounded agent, and about the most expensive loop shape of all in Computer use agents are a last resort.
I would not grant an agent spend authority on the strength of a margin number from a single call. Get the p90 across your fixture file first.
When it doesn't work
openai.AuthenticationError: Error code: 401 — Incorrect API key provided
The key is not in this shell. export applies per shell session, so a new tab or a new tmux pane has nothing. Re-export it, or put it in a .env file and load it with python-dotenv. If the key string itself is wrong, revoke and reissue at platform.openai.com/api-keys rather than editing it by hand — a truncated paste produces the same 401.
openai.RateLimitError: Error code: 429 — You exceeded your current quota
with "type": "insufficient_quota". The key is valid and the account has no usable credit. This is not a rate limit despite the class name, and retrying will not clear it. Add credit in your platform billing settings, then re-run. A genuine rate limit returns 429 with a different message about requests per minute; that one you fix with backoff.
KeyError: 'gpt-5-mini' (or whichever model you switched to)
Raised from PRICES[model] in price_call. The script refuses to price a model it has no published rate for. That is deliberate — a default rate would silently produce a wrong margin, and a wrong margin you trust is worse than a crash. Copy the input, cached-input and output rates from the pricing page and add the row.
The quiet one that produces no error at all: skip the cached_tokens subtraction, bill every prompt token at the full input rate, and your cost comes out high while your margin comes out pessimistic. With a long stable system prompt and caching active, that gap is not rounding. Check that cached_input_tokens is non-zero on repeat runs of an identical prefix before you trust a cost comparison between two prompt designs.
What this number leaves out
Inference is one line. The script does not count the eval runs you fire across those 20 fixtures on every prompt change, the retries that never reach the user, the embedding and re-index cost when your source data changes, the human who reviews the output before it ships, or support time on the cases the model got wrong. On a feature with a review step, the reviewer costs more than the tokens.
Failed runs bill as well. A call that times out after generating 900 output tokens, or an agent that burns through its step cap and returns nothing a user can act on, costs the same as one that succeeds: providers charge for tokens produced, not for outcomes delivered. So a 5 percent failure rate makes your real cost per delivered result roughly 5 percent higher than anything this script prints, and a feature with a retry-on-failure policy pays that twice. Log cost against the outcome, not just against the request.
Building the thing and pricing the thing pull in different directions — that split is the subject of An AI entrepreneur is two different jobs, not one.
Account-level spread is the variable that decides everything else. Measure it before you publish a price.
Sources
- OpenAI API pricing — Per-million-token input, cached-input and output rates used in the PRICES dict.
- OpenAI API keys — Where the reader creates the key the script reads from OPENAI_API_KEY.
- openai-python SDK — The client library installed in step 2 and the usage object the script reads.
- OpenAI prompt caching guide — Cached input tokens are reported inside prompt_tokens and billed at a reduced rate.
- OpenAI Batch API — Batch requests are discounted relative to synchronous calls.
- Anthropic prompt caching — Cache writes bill at 1.25x base input, cache reads at 0.1x base input.
- ChatGPT pricing — Per-seat subscription example: ChatGPT Plus at $20/month.
- GitHub Copilot — Per-seat developer-tool pricing example.
- Stripe usage-based billing — Meters and meter events for billing a metered unit of work.