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.
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;
}
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
}
Get weekly developer tips
Join 25,000+ developers. Practical guides, job tips, and new content — straight to your inbox.
No spam. Unsubscribe anytime.
See audit trails for AI-initiated financial actions for audit log design.
KES 2,999
The definitive guide to building payment systems with Paystack — from your first transaction to production-grade payment infrastructure across Africa. 9 modules, 47 lessons. Free preview available.
Start Paystack Course