Agentic Payouts: Safety Rails You Must Build First
Required safety rails for agentic Paystack payouts: 1) Maximum single transfer limit (e.g., NGN 50,000 per transfer — hard-coded, not AI-configurable). 2) Daily transfer cap (total per day). 3) Recipient allowlist (agent can only transfer to pre-approved recipient_codes). 4) Approval queue for transfers above a threshold. 5) Dry-run mode for testing. 6) Immutable audit log (who/what triggered each transfer). 7) Rate limit (max N transfers per minute). 8) Balance check before dispatch.
Safety Rails Implementation
const PAYOUT_LIMITS = {
maxSingleTransfer: 50_000_00, // NGN 50,000 in kobo — hard limit
maxDailyTotal: 500_000_00, // NGN 500,000 per day
allowedRecipients: new Set(['RCP_xxxxx', 'RCP_yyyyy']), // pre-approved recipients only
maxPerMinute: 5,
};
var _transfersThisMinute = 0;
var _transfersReset = Date.now();
async function agentTransfer({ recipientCode, amount, reason, agentSessionId, dryRun = false }) {
// 1. Check recipient allowlist
if (!PAYOUT_LIMITS.allowedRecipients.has(recipientCode)) {
throw new Error('BLOCKED: Recipient not in allowlist');
}
// 2. Check single transfer limit
if (amount > PAYOUT_LIMITS.maxSingleTransfer) {
throw new Error('BLOCKED: Amount exceeds per-transfer limit');
}
// 3. Check daily cap
var todayTotal = await db.agentTransfers.sumToday();
if (todayTotal + amount > PAYOUT_LIMITS.maxDailyTotal) {
throw new Error('BLOCKED: Would exceed daily cap');
}
// 4. Rate limit
if (Date.now() - _transfersReset > 60000) { _transfersThisMinute = 0; _transfersReset = Date.now(); }
if (_transfersThisMinute >= PAYOUT_LIMITS.maxPerMinute) {
throw new Error('BLOCKED: Rate limit hit');
}
// 5. Check balance
var balance = await getPaystackBalance();
if (balance < amount + 10000) throw new Error('BLOCKED: Insufficient balance');
// 6. Audit log (before transfer)
var auditId = await db.auditLog.insert({ agentSessionId, recipientCode, amount, reason, dryRun, ts: new Date() });
if (dryRun) return { status: 'dry_run', auditId };
// 7. Execute transfer
var res = await fetch('https://api.paystack.co/transfer', {
method: 'POST',
headers: { Authorization: 'Bearer ' + process.env.PAYSTACK_SECRET_KEY, 'Content-Type': 'application/json' },
body: JSON.stringify({ source: 'balance', amount, recipient: recipientCode, reason, reference: 'agent_' + auditId }),
});
_transfersThisMinute++;
var data = await res.json();
await db.auditLog.update(auditId, { transferCode: data.data?.transfer_code, status: data.status ? 'sent' : 'failed' });
return data;
}
Approval Queue for Large Transfers
For transfers above a threshold (e.g., NGN 100,000), queue for human approval instead of executing immediately:
const APPROVAL_THRESHOLD = 100_000_00; // NGN 100,000 in kobo
async function agentTransferWithApproval(params) {
if (params.amount > APPROVAL_THRESHOLD) {
var requestId = await db.pendingTransfers.insert({ ...params, status: 'awaiting_approval' });
await sendSlackAlert('#finance-approvals', 'Agent payout request: NGN ' + params.amount/100 + ' to ' + params.recipientCode + '. Approve: /approve/' + requestId);
return { status: 'queued', requestId };
}
return agentTransfer(params); // execute immediately for small amounts
}
Learn More
See audit trails for AI-initiated financial actions for audit log design.
Key Takeaways
- ✓Hard-code a per-transfer maximum amount that the AI cannot override.
- ✓Maintain a daily transfer cap and check it before every transfer — not just once at startup.
- ✓Restrict the AI agent to a recipient allowlist — it cannot create new recipients autonomously.
- ✓Log every AI-initiated transfer with: agent session ID, prompt that triggered it, amount, recipient, timestamp.
- ✓Implement a dry-run mode for testing — transfers are validated but not executed.
Frequently Asked Questions
- Can the AI agent itself be configured to raise its own transfer limits?
- No — and the code must enforce this. Limits must be hard-coded constants or environment variables that only a human administrator can change. Never pass limits as parameters to the agent or let the agent modify them via tool calls. An agent that can raise its own limits has no limits.
- What should happen when the AI agent hits a safety rail?
- The tool should throw a descriptive error ("BLOCKED: Amount exceeds per-transfer limit"), the AI agent should receive this as a tool error, and a human-readable message should be logged. The agent should surface the block to the human operator ("I tried to send NGN 200,000 but the limit is NGN 50,000 — please approve this manually"). It should NOT retry with a different approach to circumvent the limit.
- Should I use different API keys for my AI agent vs. my application?
- Yes. Create a separate Paystack account team member or use a separate integration with restricted permissions if Paystack supports it. At minimum, rotate the key used by the AI agent independently from your main application key. This allows you to revoke the agent's access without affecting your application.
Ready to build real-world apps?
Join the McTaba Labs full-stack marathon (4 months full-time · 6 months part-time). Learn M-Pesa, USSD, and WhatsApp engineering while shipping 8 production apps.
Apply to the McTaba Marathon