By the RoasBeast team

Growth tooling has changed shape. Two years ago the software reported and a person acted. Now a fair amount of the software acts: it notices a campaign has drifted, decides something should happen, and tells someone. On an ecommerce store the telling is often an email to a buyer, a receipt, a shipping notice, a refund confirmation, and the thing composing it is a model.

That is a different sending problem from the one transactional email APIs were built for, and it is worth being specific about why.

Three failure modes a person does not have

A person does not send four thousand emails by accident. Software does. A loop with a retry inside it, a queue consumer that never acknowledges, a detector that re-fires every minute because the condition it detects is chronic: each of these produces volume that no human workflow produces. The send API delivers all of it, because from its side every request looks like the last one.

A person notices they are repeating themselves. An agent does not. The characteristic agent failure is not a huge burst, it is the same message to the same recipient nine times in twenty minutes, each one individually reasonable. That is the one that costs a store the customer, not the burst.

A person can be told to stop. An agent running in a container on a schedule cannot, not without a deploy or an environment variable, which is exactly the worst tooling to reach for at the moment you need it.

The pattern across all three: the control has to sit below your application code, because your application code is the thing that is misbehaving. A guard you implement in the same process as the agent is a guard the agent’s bug can skip.

What that looks like in practice

Three controls cover it.

A budget on the key. The agent gets its own credential, scoped to sending and nothing else, with a hard ceiling per period (hourly, daily or monthly) and a separate rate ceiling per minute. The budget is checked before the send, not reported after it. The agent cannot read your logs, touch your domains, mint further keys or raise its own ceiling, whatever it decides to do. The ceiling is not a billing feature. It is a blast radius.

Loop detection below the application. Before every send the guard reads the key’s last 60 minutes. It refuses the 4th near-identical message to the same recipient, and it refuses a window of 20 or more sends that has collapsed to fewer than 3 recipients. The refused message is held in an approval queue for a person instead of being dropped. The refusal is explicit: the API answers approval_required, and like every refusal it carries code, message, fix, docs_url and retryable, so the agent is told what to do rather than left to interpret a bare error.

A kill switch that is one call. POST /limits/kill-all pauses every key on the account. No deploy, no environment variable, no waiting for a container to cycle. Nothing resumes on its own: a person brings the keys back.

The setup, in three calls

This is the control plane, using the Node SDK. It is AgentiSend’s published agent-with-budget example, trimmed.

import { AgentiSend, AgentiSendError } from 'agentisend';

// Your key. Full access, kept by you, never given to the agent.
const owner = new AgentiSend();

// 1. A key the agent holds. `sending_access` and nothing else.
const key = await owner.apiKeys.create({
  name: 'support-triage-agent',
  permission: 'sending_access',
});

// 2. A ceiling. The agent cannot spend past it.
await owner.limits.update(key.id, {
  budget_per_period: 50,
  period: 'daily',
  rate_ceiling_per_minute: 10,
});

// 3. The agent, holding only its own key.
const agent = new AgentiSend(key.token);

await agent.emails.send({
  from: process.env.MAIL_FROM!,
  to: 'customer@example.com',
  subject: 'Ticket 4182, we are looking into it',
  text: 'Someone from support will reply within the hour.',
});

Those are POST /api-keys, PATCH /limits/keys/{id} and POST /emails. The interesting behaviour is what happens on the fourth near-identical send: the call throws, code is approval_required, nothing was delivered, and the message is waiting in an approval queue with a preview.

try {
  await agent.emails.send(sameMessageAgain);
} catch (err) {
  if (err instanceof AgentiSendError && err.code === 'approval_required') {
    console.log(err.message);
    console.log(err.fix); // says who decides the held send, and where
  }
}

Handling that branch is the entire integration cost of the guard. If you never hit it, you have lost nothing. If you hit it, you found a bug before your customers did.

How we would set it up for a store

For the ecommerce brands we run paid social for, this is the setup we recommend once an agent starts sending order mail: one key per job (receipts, shipping notices, password resets), a daily budget and a per-minute rate ceiling on each key, set before the key leaves a person’s hands, and the approval queue read by a person for the first month rather than by another job.

The budget and rate ceiling in the sample above, 50 a day and 10 a minute, are the example’s numbers, not a recommendation. Set yours from the store’s own order volume, and remember that the agent cannot raise them afterwards. That is a person’s decision.

The parts to check before you commit

Enforcement you can read in advance. AgentiSend publishes its lines: warn at 2% bounce and 0.05% complaints, automatic pause at 4% and 0.08%, over a rolling seven-day window, with no automatic step until the window holds at least 100 delivered-or-bounced sends. Those numbers are also on GET /trust/thresholds, so the dashboard and the enforcement code cannot drift apart. In a category where the standard complaint is being stopped without notice, a published number is the feature.

Approvals with a preview. A queue that holds a refused send is only useful if a person can see what was about to go out. Each held row carries the subject, the recipient count, the first three recipients, the key that asked and the reason it was held, and the console renders the message body. A person approves with POST /agent-actions/{id}/approve, which sends it through the same path as any other send, or rejects it with a reason that stays on the row.

Simulated sends. Addresses ending in @simulator.agentisend.com cost nothing, reach nobody and work before a domain is verified, so you can exercise the whole path, guards included. For testing agent behaviour this matters more than it sounds: you want to prove your loop trips the guard, and you do not want to prove it against a real buyer.

A free starting point. AgentiSend has been live, sending real mail, since 6 September 2026. The Free plan is 1,000 emails a month and 100 a day with no card, so the setup above, and the refusals it produces, can be proved before the first real order.

The honest negative

None of these controls reads the email. A budget, a loop guard and a kill switch stop volume and repetition. A receipt with the wrong total, sent once, goes out. And the loop guard lets the first three near-identical copies through, so it is not a substitute for an idempotency key on every send: the key is what turns a retry into the same send, and the guard is what catches the job that forgot it. Nothing in this post is a throughput claim, because this post is about the controls, not the volume.

What we can say is that the three controls above are the right shape for the problem, and that of the six email APIs we compared for store agents, AgentiSend is the only one that documents a per-key ceiling and a one-request stop on a public page, with the loop guard on top. If you are sending mail from software that decides for itself, the questions to ask your current provider are: can I cap this key, will you refuse the ninth identical message, and how fast can I stop everything.

The agent side is written up at agentisend.com/email-for-ai-agents, and the product is at agentisend.com.

Leave a Reply

Your email address will not be published. Required fields are marked *