Gil Allouche
← All writing
TutorialEntrepreneurship in the age of AI

Charge for one agent task: a working metered-billing build

Build a script that runs one agent task, reads its token usage, posts a billable unit to a Stripe test meter, and prints the gross margin on that single task.

October 26, 2026·Gil Allouche·10 min read
A build-along. Every step was run start to finish; commands and outputs are what the machine actually printed.

What you'll have running

181 input tokens, 34 output, $0.000127 of model cost, billed to Stripe as one $0.25 unit. That is what the script on this page prints when it finishes: it runs an agent task, reads the real token counts off the API response, converts the task into a single billable unit, posts that unit to a Stripe meter in test mode, and reports the gross margin. About sixty lines. It is also the entire revenue machine of most AI companies.

Budget an hour if your Stripe account is new. Almost none of that hour is code — it goes into creating the meter and the test customer.

You need Python 3.11 or newer and pip install --upgrade openai stripe, an OpenAI API key from platform.openai.com/api-keys, and a Stripe test mode secret key from dashboard.stripe.com/test/apikeys — it starts with sk_test_, and if yours doesn't, go back and switch the dashboard toggle before you do anything else. You also need the current input and output price per million tokens for whichever model you pick, from openai.com/api/pricing, and a terminal with curl.

Nothing here charges a real card. Stripe test mode is a parallel set of data with its own keys, and the invoices it produces are fake on purpose.

The four units AI companies actually bill

"How do AI startups make money" has four production answers, and they differ by what the invoice line says, not by business model philosophy.

Billed unitWhat the line item readsPublic example
Tokeninput and output tokens, usually per millionOpenAI's API price list, billed per million input and output tokens
Seatone user, one monthGitHub Copilot, listed per user per month on its plans page, with Business listed at $19 per user per month
Outcomeone resolved thingIntercom lists Fin at $0.99 per resolution
Seat + metered overagea subscription, plus usage above the included allowanceCursor's plans pair a monthly seat with usage-based charges — Pro is listed at $20 per month

Resellers of raw inference bill tokens. That unit is honest and nearly useless to a buyer, because it makes the customer forecast something they cannot see — the number of tokens one of their actions costs. Companies selling a workflow bill seats, which is the easiest unit to sign and the one that tells you least about whether the product is being used. Companies confident in their own accuracy bill outcomes, because an outcome is the only unit a buyer can audit against their own numbers.

Outcome pricing is the strongest position and the most dangerous one to take early. This is a view, not a measurement. You are promising that your cost per attempt times your retry rate stays below your price per success, and if the agent needs three attempts to resolve one ticket, you billed once and paid three times. That is the arithmetic the rest of this build makes visible.

Step 1 to 5: build it

  1. Create the meter. curl https://api.stripe.com/v1/billing/meters -u "$STRIPE_API_KEY:" -d display_name="Agent tasks" -d event_name="agent_task" -d "default_aggregation[formula]"=sum — the response is a JSON object with "object": "billing.meter", an id starting mtr_, and "status": "active". Save the mtr_ id; you need it in step 5. The event_name is the string your code will send, and it is matched exactly, case-sensitive.
  2. Create a test customer. curl https://api.stripe.com/v1/customers -u "$STRIPE_API_KEY:" -d name="Northwind Tools" -d email="billing@example.com" — you get back an id starting cus_. Export both ids plus your prices: export STRIPE_CUSTOMER_ID=cus_..., export PRICE_IN_PER_MTOK=0.40, export PRICE_OUT_PER_MTOK=1.60, export PRICE_TO_CUSTOMER=0.25. Use the real numbers from the pricing page for your model. Those three are placeholders, not quotes.
  3. Save the script below as meter_one_task.py. Four things in order: call the model, read usage, post one meter event, print the margin.
  4. Run it. python meter_one_task.py — you should see the model's JSON answer, then four lines of accounting, then a meter event identifier beginning task-. If the last line prints, Stripe accepted the usage.
  5. Confirm Stripe recorded it. curl -G https://api.stripe.com/v1/billing/meters/mtr_YOURID/event_summaries -u "$STRIPE_API_KEY:" -d customer=cus_YOURID -d start_time=1740000000 -d end_time=1740003600 with an hour-aligned window covering your run. A successful response has a data array containing one summary object with "aggregated_value": 1. An empty data: [] means the event has not landed yet or the customer id does not match — see the failure section.

Here is the script. It is complete; no placeholders beyond the environment variables.

# meter_one_task.py
# Runs one agent task, prices it, bills one unit to a Stripe meter, prints margin.
import json
import os
import uuid

import stripe
from openai import OpenAI

MODEL = "gpt-4.1-mini"
EVENT_NAME = "agent_task"

PRICE_IN_PER_MTOK = float(os.environ["PRICE_IN_PER_MTOK"])
PRICE_OUT_PER_MTOK = float(os.environ["PRICE_OUT_PER_MTOK"])
PRICE_TO_CUSTOMER = float(os.environ["PRICE_TO_CUSTOMER"])
CUSTOMER_ID = os.environ["STRIPE_CUSTOMER_ID"]

client = OpenAI()                      # reads OPENAI_API_KEY
stripe.api_key = os.environ["STRIPE_API_KEY"]

LEAD = {
    "task_id": "lead-8814",
    "name": "Dana Okafor",
    "email": "dana@example.com",
    "company": "Northwind Tools (example.com)",
    "note": "Downloaded the pricing PDF, 40 employees, asked about SSO.",
}

SYSTEM = (
    "Score this inbound lead from 0 to 100 and give one reason. "
    'Reply as JSON: {"score": int, "reason": str}'
)


def run_task(lead):
    r = client.chat.completions.create(
        model=MODEL,
        messages=[
            {"role": "system", "content": SYSTEM},
            {"role": "user", "content": json.dumps(lead)},
        ],
        response_format={"type": "json_object"},
    )
    u = r.usage
    return r.choices[0].message.content, u.prompt_tokens, u.completion_tokens


def cost_usd(in_tok, out_tok):
    return (in_tok / 1_000_000) * PRICE_IN_PER_MTOK + (
        out_tok / 1_000_000
    ) * PRICE_OUT_PER_MTOK


def bill_one_unit(customer_id, identifier):
    # identifier makes the write idempotent: a retry with the same
    # string does not create a second billable unit.
    return stripe.billing.MeterEvent.create(
        event_name=EVENT_NAME,
        payload={"stripe_customer_id": customer_id, "value": "1"},
        identifier=identifier,
    )


if __name__ == "__main__":
    answer, in_tok, out_tok = run_task(LEAD)
    cost = cost_usd(in_tok, out_tok)

    # Derive the identifier from the task, not from uuid4(), once this is real.
    identifier = f"task-{LEAD['task_id']}-{uuid.uuid4().hex[:8]}"
    event = bill_one_unit(CUSTOMER_ID, identifier)

    margin = PRICE_TO_CUSTOMER - cost
    print(answer)
    print(f"tokens in/out       : {in_tok}/{out_tok}")
    print(f"model cost USD      : {cost:.6f}")
    print(f"charged USD         : {PRICE_TO_CUSTOMER:.2f}")
    print(f"gross margin USD    : {margin:.6f} ({margin / PRICE_TO_CUSTOMER * 100:.1f}%)")
    print(f"stripe meter event  : {event.identifier}")

What you should see, with your own token counts in place of these:

{"score": 78, "reason": "Mid-size company, pricing intent, asked about SSO."}
tokens in/out       : 181/34
model cost USD      : 0.000127
charged USD         : 0.25
gross margin USD    : 0.249873 (99.9%)
stripe meter event  : task-lead-8814-3f9c21ab

That 99.9% is honest for one successful call and misleading for a business. It is the number before retries, before the context you will add, before the human who reviews the low-confidence cases. First column of a P&L. Not the whole thing.

Where the margin actually goes

The model call is the cheapest line in an agent product.

Retries come first. One billable unit can consume three model calls or ten, and if you bill per outcome, every failed attempt is pure cost. Unbounded loops are the most common production failure in agent systems. The fix is a step cap enforced outside the model — a counter in your own code that raises before call N+1, not an instruction in the prompt asking the model to stop. A cap of 10 calls on a task that averages three bounds your worst case at roughly 3.3× your average cost, which is a number you can defend in a price; without the cap the worst case is unbounded and the price is a guess. I wrote about why that cap belongs in the control layer rather than the framework in agent orchestration is a control layer, not a framework.

Then context growth. Input tokens cost less per token than output, and they are where volume accumulates: tool definitions, retrieved documents, conversation history. The 181-token prompt above costs $0.000072 of input at $0.40 per million; the same task carrying 40,000 tokens of retrieved context costs $0.016, about 126× the entire call you just measured. Anthropic's prompt caching documentation describes cached input tokens being billed at a different rate from uncached input — cache reads at 0.1× the base input price, cache writes at 1.25× for the five-minute TTL — which is why the same conversation can cost materially different amounts depending on whether you reuse a stable prefix. OpenAI's Batch API guide documents a 50% discount for jobs you are willing to run asynchronously inside a 24-hour completion window. Neither helps if your agent is interactive and your prefix changes every turn.

Then humans. A review queue is a cost per unit, same as tokens — if a human looks at 20% of outcomes for two minutes each, that cost belongs in cost_usd() as a second term. At a fully loaded $30 an hour, that is 0.4 minutes and $0.20 per billed unit, which turns the 99.9% margin printed above into roughly 20% on a $0.25 unit. Most pricing models I have seen break here, not on inference.

A priced agent task: run, read usage, post a meter event, invoice — and the retry path that double-billsTask requestAgent runusage tokenstokens to USDMeter eventInvoice lineretry, no identifier= billed twice

When it doesn't work

stripe.error.InvalidRequestError with code resource_missing. The message names the meter or the customer. Almost always the event_name in your code does not byte-match the event_name you created in step 1 — agent_task versus agent-task versus Agent_task. List your meters with curl https://api.stripe.com/v1/billing/meters -u "$STRIPE_API_KEY:" and copy the string out of the response rather than retyping it. The same code appears if STRIPE_CUSTOMER_ID is from live mode while the key is sk_test_. Test and live objects do not cross over.

The script prints a meter event identifier, but event_summaries returns "data": []. Two causes. Meter events are ingested asynchronously, so a summary query issued a second after the write can legitimately show nothing — wait and re-run. The other cause is the time window. The summaries endpoint wants an hour-aligned range, and an unaligned start_time comes back as a 400 invalid_request_error rather than an empty array, so an empty array returned with a 200 means your window is valid but does not contain the event. Widen it to cover the full hour of your run and check the customer id matches the one in payload.

The same task gets billed twice and nothing errors. This is the failure with no error text, which is why it reaches invoices. Any retry — your own loop, a queue redelivery, a user double-click — posts a second meter event unless the identifier is derived from the task. uuid.uuid4() in the sample is deliberately wrong for production: it is unique per call, not per task. Replace it with something stable, like f"task-{lead['task_id']}", so a redelivery collides with the original and Stripe deduplicates it. Test it by running the script twice with a hard-coded identifier and confirming aggregated_value stays at 1.

KeyError: 'PRICE_IN_PER_MTOK'. The export did not survive into the shell you ran Python from. Check with echo $PRICE_IN_PER_MTOK.

openai.AuthenticationError: Incorrect API key provided. OPENAI_API_KEY is unset or truncated. The SDK reads it from the environment automatically; you never pass it in code.

What to meter next, and what not to

Once one unit bills correctly, the interesting work is choosing the unit. Meter the thing the customer would count on their own: resolved tickets, enriched records, scored leads, approved invoices. Do not meter "API calls" if your customer has no idea how many calls one of their actions produces — a $0.25 unit they can tie to a lead is defensible, and a line that reads "412,900 API calls" generates a support ticket every month.

Two things I would not wire up yet. I would not give an agent spend authority over its own inference budget; a cap enforced in your code is cheaper than a conversation with your CFO. And I would not bill an outcome you cannot verify programmatically, because the first dispute will be resolved by whoever has the log, and if neither side has one, it is resolved by refund.

If you want the task itself to be something more useful than a lead score, the pipeline in build a lead triage agent that scores inbound in one pass drops straight into run_task(), and get into the AI business by shipping one capped agent covers the cap that keeps the retry column from eating the margin column.

Sources

  1. Stripe: usage-based billing — Meters, meter events, and how metered usage becomes an invoice line
  2. Stripe API reference — billing/meters and billing/meter_events endpoints and parameters
  3. Stripe: test mode and test data — Running the whole flow without moving real money
  4. Stripe test API keys — Where the reader gets STRIPE_API_KEY
  5. Stripe error codes — resource_missing and other 400 codes referenced in the failure section
  6. OpenAI Chat Completions API reference — The usage object with prompt_tokens and completion_tokens
  7. OpenAI API pricing — Per-million-token input and output prices the reader fills into the config
  8. OpenAI API keys — Where the reader gets OPENAI_API_KEY
  9. OpenAI Batch API guide — Documented discount for asynchronous batch jobs
  10. Anthropic prompt caching — Cached input tokens are billed differently from uncached input
  11. GitHub Copilot plans — Per-seat pricing example
  12. Intercom pricing — Fin priced per resolution — outcome-based example
  13. Cursor pricing — Seat plus metered usage hybrid example

Related