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

> Source: <https://dev.to/awesomevideo/how-a-paid-mcp-server-can-charge-ai-agents-safely-what-we-learned-building-one-2c0l>
> Published: 2026-11-09 14:20:05+00:00

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.

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
}
```

`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.

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":

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"
}
```

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:

`requiredCents` and `balanceCents` let the model explain the situation in one sentence.
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.

Agents retry, so a retry must never charge twice:

`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.`cached: true` and `costCents: 0`.` resume_job` continues from saved steps instead of paying for everything again.`UPDATE` that only succeeds if the balance still covers it, so two parallel calls can't both spend the same cents.
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:

`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.`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.

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](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.
