Use cases · Published 2026-09-29 · AgentiSend team
Agent daily digest
An agent sends one digest a day to people who already receive it. The same digest sent again inside the window is held.
An agent sends one digest a day to people who already get that digest from you. Each recipient is one send, with that day's subject and body, from a verified domain.
The ceiling
PATCH /limits/keys/{id} for the digest key:
{
"budget_per_period": 100,
"period": "monthly",
"rate_ceiling_per_minute": 5
}One hundred is the count of digest copies in the monthly period, not a suggestion to add recipients. It is under the Free inclusion of 1,000. The rate ceiling is five a minute.
The send
/**
* Vercel AI SDK — a `sendEmail` tool for an agent that holds its own key.
*
* The key in `AGENTISEND_API_KEY` here is the agent's, not yours. It was
* minted with `POST /api-keys` as `sending_access`, given a ceiling with
* `PATCH /limits/keys/:id`, and `POST /limits/kill-all` stops it along with
* every other key. Those three calls run in examples/ai-agent-with-budget;
* this file is the other half, the tool the model calls once the key exists.
*
* Two rules make it safe to hand to a model. The idempotency key is derived
* from the purpose and the recipient, never from the moment, so a model that
* retries a timed-out call replays the first send instead of mailing someone
* twice. And a refusal is returned, not thrown: the model reads `code` and
* `fix` and acts on them — for `agent_budget_exceeded` and `approval_required`
* that means stopping and saying so, because the fix names a person.
*/
import { tool } from 'ai';
import { z } from 'zod';
import { AgentiSend, AgentiSendError } from 'agentisend';
/** The agent's own key: `sending_access`, with a budget on it. */
const agentisend = new AgentiSend();
function mailFrom(): string {
const from = process.env.MAIL_FROM;
if (!from) throw new Error('Set MAIL_FROM to an address on a domain you have verified.');
return from;
}
/** What the model gets back: a message id, or a refusal it can act on. */
export type SendEmailResult =
| { sent: true; id: string }
| { sent: false; code: string; fix: string };
export const sendEmail = tool({
description:
'Send one email to a person who asked for it. Returns the message id, or a refusal ' +
'with a code and a fix. When the fix says a person decides, stop and report it: ' +
'calling again will not change the answer.',
inputSchema: z.object({
to: z.string().describe('The recipient, one address.'),
subject: z.string().min(1).max(200),
text: z.string().min(1).describe('The plain-text body.'),
purpose: z
.string()
.regex(/^[a-z0-9][a-z0-9_-]{0,63}$/)
.describe(
'What this message is, for example "ticket-4182-update". The same purpose to the ' +
'same recipient sends once, however many times it is called.',
),
}),
execute: async ({ to, subject, text, purpose }): Promise<SendEmailResult> => {
try {
const { id } = await agentisend.emails.send(
{ from: mailFrom(), to, subject, text },
// Derived from the thing being done, not from the moment it was asked
// for: a retry after a timeout replays the first send.
{ idempotencyKey: `${purpose}/${to}` },
);
return { sent: true, id };
} catch (err) {
if (err instanceof AgentiSendError) {
// The API checked the address, the suppression list, the budget and
// the loop guard, and the refusal names its fix. The model can read
// that; it cannot read an exception.
return { sent: false, code: err.code, fix: err.fix };
}
throw err;
}
},
});Derive the idempotency key from the day and the recipient, the way that file derives it from the purpose. A retry of the same digest replays the first send.
The refusal
Spending the key's budget refuses the next copy. The catalogue fix:
{
"error": {
"code": "agent_budget_exceeded",
"message": "This key has used 1 of its 1 recipients for the current monthly period. Each To, CC and BCC recipient counts as one email.",
"fix": "Wait for the period to reset — get_agent_budget and GET /limits/keys/:id both say when. Raising a budget is a person’s decision, made in the console; a key cannot raise its own.",
"docs_url": "https://agentisend.com/what-does-agent-budget-exceeded-mean#agent_budget_exceeded",
"retryable": false
}
}Honest limit
Sending the same digest again to the same address inside 60 minutes is held once it would be the 4th copy. Reusing an idempotency key with a different body is idempotency_payload_mismatch, and the fix is to send the same body or a new key.