DEV Community

AwesomeVideo
AwesomeVideo

Posted on

How a paid MCP server can charge AI agents safely (what we learned building one)

I'm the maker of AwesomeVideo, a hosted captioning API and MCP server that agents pay for with prepaid credits. This post is about the billing side, not the video side. Everything below is how our server behaves today, including the parts I'm not sure we got right.

When your tool costs real money per call, the caller isn't a person reading a pricing page. It's a model that will retry on a timeout, misread a number, or chain two calls you hoped it would keep apart. Here's the design we ended up with, in the order a request goes through it.

1. Make the price its own free call

Our first rule: an agent should never have to spend money to learn what something costs. We have two free endpoints that need no key at all: POST /v1/quote for a single operation and POST /v1/plan for a whole pipeline. Over MCP, the same thing is the estimate tool.

A real response for an 8-second clip:

{
  "operation": "caption",
  "audioSeconds": 8,
  "costCents": 2,
  "breakdown": { "transcribe": 1, "burn": 1 },
  "centsPerMinute": 5,
  "charged": false
}
Enter fullscreen mode Exit fullscreen mode

plan also returns limitViolations (too long, too big, unknown duration) before anything runs. Paid tools check the same limits and return 422 limit_exceeded with charged: false instead of failing halfway.

We chose a separate tool called estimate over a dryRun flag on each paid tool. Our bet: a model follows "call estimate first" more reliably than it remembers an optional flag.

Tradeoff: each step is rounded up to a whole cent on its own. An 8 s caption costs 2 credits (1 for transcription, 1 for the burn), while a full minute costs 5. Short clips pay a rounding premium. We'd rather be predictable than charge fractions, but it's a real cost to small jobs.

2. Prepaid credits, and the agent can't top up by itself

1 credit = 1 cent, bought ahead of time. The agent has no way to charge a card. That's the most important safety property we have, and it follows from "agent spends, human funds":

  • The worst an agent can do is spend the balance a human already put in.
  • More money always needs a human in a browser.

When the balance is short, the paid call fails before a job starts, with HTTP 402. This is the shape our code builds (trimmed, the checkout link is a placeholder):

{
  "error": "insufficient_balance",
  "message": "Captions (transcribe + burn) costs 2 cents. This key has 0 cents. No job was started and nothing was charged. Open <checkoutUrl> to buy 500 credits for $5 for this key (no sign-in needed; 1 credit = 1 cent). …",
  "balanceCents": 0,
  "requiredCents": 2,
  "shortfallCents": 2,
  "quote": { "operation": "Captions (transcribe + burn)", "requiredCents": 2, "currency": "usd", "credits": 2 },
  "checkoutUrl": "<checkout link tied to this key>",
  "checkoutTiedToKey": true,
  "pricingUrl": "https://awesomevideoeditor.com/pricing"
}
Enter fullscreen mode Exit fullscreen mode

Over MCP, the same body comes back as a tool result with isError: true, plus two fields for the model: hint ("Nothing was charged. Show checkoutUrl to your human, or call create_checkout; then retry the same call.") and retryable: true.

Three details we'd keep:

  • Say "nothing was charged" out loud. Then the model doesn't have to guess whether a retry would pay twice.
  • Tie the checkout to the key. The human pays, and the credits land on the exact key the agent is already using, with no account linking step.
  • Give numbers, not just a code. requiredCents and balanceCents let the model explain the situation in one sentence.

3. Let the caller cap the job

Every paid call accepts maxCredits. If the real price (worked out after we probe the media's duration) is above it, the call returns 409 max_credits_exceeded and charges nothing. The agent sets it from what the user agreed to, so a misread quote can't turn into a bigger bill.

For keys that belong to an account, the human can also set a monthly spend cap per key when creating it on the account page. Past that cap, calls return a 402 key_spend_cap. That one lives outside the model's reach.

4. Make retries free

Agents retry, so a retry must never charge twice:

  • Idempotency: REST takes an Idempotency-Key header and MCP tools take idempotencyKey. The same key on the same API key returns the original job instead of creating a new one.
  • Cache: results are cached per key and per media hash plus options. A repeat of a finished job returns a copy with cached: true and costCents: 0.
  • Refunds: if a job fails after charging, it's refunded automatically to the same balance. resume_job continues from saved steps instead of paying for everything again.
  • Atomic debit: the charge is a single conditional UPDATE that only succeeds if the balance still covers it, so two parallel calls can't both spend the same cents.

5. Free trials without a free-for-all

We wanted an agent to go from zero to a first result with no human. That means giving away credits to anonymous callers, which is how free tiers get drained.

What we do today:

  • A new self-serve key can get 10 welcome credits, valid for 30 days, and only for transcription/caption jobs up to 2 minutes. They can't be spent on anything else.
  • To claim them, the caller solves a small proof-of-work: GET /v1/keys/challenge returns an HMAC-signed challenge (20 bits right now, valid 10 minutes, single use), and you find a nonce so that SHA-256(challenge + ":" + nonce) starts with that many zero bits. That's a second or two of CPU for one honest agent and real cost for a farm.
  • Grants are rationed per network and per network operator per day, out of daily and monthly budgets, released in slots through the day. When the pool is empty, the response says why (welcomeCreditsReason, e.g. slot_wait with nextSlotAt) instead of silently giving zero.

Tradeoff: proof-of-work is friction, and some headless clients will never run a solver. For OAuth client_credentials clients (registered with client_secret_post), we skip it and give a smaller automatic grant of 5 credits under its own daily cap. It's a compromise: less abuse-proof, but it doesn't punish machine-to-machine callers.

6. What I'd tell someone starting today

  • Put prices in a free tool and in every error.
  • Make the human the only one who can add money.
  • Give the agent a cap parameter and give the human a cap the agent can't touch.
  • Make retries free by design (idempotency, cache, refunds) before you need them.
  • Assume every free credit will be farmed, and decide up front what you'll ration by.

One open question we still argue about: does a quote count as consent? Within the prepaid balance we let the model decide, and the human decides only at checkout. If your tool is destructive rather than just costly, MCP elicitation (a confirmation the client shows mid-call) may be a better gate, though client support still varies.

If you want to poke at these responses, the docs for agents are at https://awesomevideoeditor.com/llms.txt (MCP endpoint: https://api.awesomevideoeditor.com/mcp). quote and plan are free and need no key. I'm happy to answer billing questions in the comments.

Top comments (0)