Guardrails for an agent calling paid APIs: key, cap, and what to log
An agent calling a paid media API needs three habits: an idempotency key on each write, a spend cap per run, and logs that hold ids, never signed URLs or keys.

Give every paid write an idempotency key, give every run a spend cap, and log ids rather than URLs or keys. Those three habits, taken from Sume's safe-automation guidance, are what keep an unattended agent from charging twice, charging without bound or leaking a credential into a log (Sume docs: Safe automation, read 2026-10-06).
They matter more as agents run on schedules. A person who clicks a button twice notices. A cron job that retries on a timeout does not.
The three habits
One: an idempotency key on every paid call. A retry with the same key and the same body replays the first receipt with idempotency_hit true, and you pay once. A retry with the same key and a different body fails with 409 idempotency_conflict, which is the correct refusal. Derive keys from the business event, like season, episode and version, rather than from a random value, which defeats the purpose.
Two: a spend cap on every run. Agent Completions require generation_spend_cap_usd. Format runs accept a number up to 500, and without one the Format's own cap applies. Schedules default to $1.00 and a per-run number can only lower it.
Three: log ids only. Keep request ids, run ids and job ids. Do not log API keys or the signed media URLs in receipts, which grant access to files.
Read-only first, paid second
On hosted MCP, the safe-automation page recommends OAuth mcp:read for exploration and says to grant mcp:write, or use an API key, before an agent calls paid tools such as generate_image or avatars_create. Write calls and paid calls must send idempotency_key; the older allow_write and allow_paid arguments are optional (Sume docs: Safe automation, read 2026-10-06).
In practice that means an agent can browse a workspace with a scope that cannot spend anything, and you widen the scope only for the job that needs it. Keep exploration and spending in separate steps so a log reader can tell which step charged.
The same page lists what is safe to log: request ids, job ids when necessary, high-level status and sanitized media metadata. It lists as unsafe API keys, signed URLs, raw private media URLs and large amounts of user content or transcripts. Treat the second list as a lint rule for your logging code.
The workspace comes from the key
The workspace a call bills to is decided by the API key, not by a field in the body. That is a guardrail too: an agent cannot move a charge to another workspace by naming it. It also means a team Format needs a key issued in that team's workspace, and a personal key will not do.
Give each automation its own key where you can, so a leaked or misbehaving one can be revoked without touching the others.
A wrapper that enforces them
Put the habits in one function so the agent code cannot forget them. The wrapper below refuses a call without a key or a cap and logs only the id.
import os, requests
def paid_post(url, body, key, cap):
if not key:
raise ValueError("idempotency key required")
if cap is None or cap <= 0:
raise ValueError("positive spend cap required")
body = {**body, "generation_spend_cap_usd": cap}
r = requests.post(url, json=body, timeout=30, headers={
"Authorization": "Bearer " + os.environ["SUME_API_KEY"],
"Idempotency-Key": key})
data = r.json().get("data", {})
print("status", r.status_code, "id", data.get("id"))
return r
try:
paid_post("https://api.sume.com/v1/agent/completions",
{"instruction": "Draft a caption."}, "cap-demo-1", 1.5)
except KeyError:
print("set SUME_API_KEY first")| Habit | Prevents | Where Sume enforces it |
|---|---|---|
| Idempotency-Key | Double charges on retry | Replay, or 409 on a changed body |
| Spend cap | Unbounded spend | Cap field, clamped on schedules |
| Ids in logs | Leaked keys and signed URLs | Your logging, not Sume's |
Review the keys quarterly
Keys age. List the ones your automations use, remove those without an owner, and rotate the rest on a schedule. A small, known set of keys is itself a guardrail.
What to do when a guardrail fires
A refusal is information. A 409 idempotency_conflict says your code changed a request it meant to repeat. A 400 about a missing cap says the wrapper was bypassed. A run that stops on its cap says the work was bigger than you planned.
Route each of these to a human or a ticket rather than a blind retry. Retrying a capped run with a larger cap in a loop turns a guardrail into a ratchet. Decide the new number once, on purpose.
Sources
Related posts
More in Agents
- Render a Short from an agent: hosted MCP tool order
Which Sume hosted MCP tools an agent calls, in order, to import, inspect, cut and render a vertical Short, with idempotency_key rules and what stays on REST.
- Scheduled run 400 because the instructions are empty: where to fix it
A Sume schedule with empty instructions can not run. The API run request returns 400 invalid_request, and the text can only be edited in the dashboard.
- Sume scheduled run returned skipped: detect previous_run_active
A Sume scheduled run that overlaps another returns 200 with status skipped, not an error. Check skip_reason in code, and choose reject if a drop must be loud.
- Did my Sume cron schedule fire? Read last_run_at and next_run_at
GET /v1/actions/{id} returns last_run_at and cron.next_run_at. Compare them with the run list to see if a schedule fired, without opening the dashboard.
Written by Sume