AI Safety & Guardrails for Token Operations
Implement validation layers, rate limiting, and approval workflows to safely let AI agents manage tokens.
Prerequisites
- Completed the Build an AI Agent with MCP tutorial
- Basic understanding of LLM tool calling
What You'll Build
AI agents are powerful, but power without guardrails is dangerous. An unchecked LLM could transfer all your tokens, delete templates, or burn through your API rate limits. In this tutorial you'll build a safety layer that wraps DUAL API calls with validation, rate limiting, human-in-the-loop approval, and audit logging — ensuring AI agents operate within safe boundaries.
Step 1 — Define Permission Tiers
Categorize every DUAL operation by risk level:
const PERMISSION_TIERS = {
// Green: AI can execute freely
read: [
'list_objects', 'get_object', 'list_templates',
'get_template', 'search_objects', 'get_balance',
'list_webhooks', 'get_sequencer_status'
],
// Yellow: AI can execute with rate limits
limited: [
'mint_object', 'update_properties',
'create_webhook', 'send_notification'
],
// Red: Requires human approval
restricted: [
'transfer_object', 'burn_object',
'delete_template', 'create_api_key',
'bulk_transfer', 'update_template'
],
// Black: AI can never execute
forbidden: [
'delete_organization', 'change_owner',
'reset_api_keys', 'modify_roles'
]
};Step 2 — Build the Rate Limiter
Prevent AI agents from making too many calls in a short period:
class RateLimiter {
constructor() {
this.windows = new Map();
}
check(operation, limits = { perMinute: 30, perHour: 200 }) {
const now = Date.now();
const key = operation;
if (!this.windows.has(key)) {
this.windows.set(key, []);
}
const calls = this.windows.get(key);
// Clean old entries
const minuteAgo = now - 60000;
const hourAgo = now - 3600000;
const recentMinute = calls.filter(t => t > minuteAgo);
const recentHour = calls.filter(t => t > hourAgo);
if (recentMinute.length >= limits.perMinute) {
return { allowed: false, reason: 'Rate limit: too many calls per minute' };
}
if (recentHour.length >= limits.perHour) {
return { allowed: false, reason: 'Rate limit: too many calls per hour' };
}
calls.push(now);
this.windows.set(key, calls.filter(t => t > hourAgo));
return { allowed: true };
}
}Step 3 — Human-in-the-Loop Approval
For restricted operations, pause execution and ask for confirmation:
class ApprovalQueue {
constructor() {
this.pending = new Map();
}
async requestApproval(operation, params, context) {
const id = crypto.randomUUID();
const request = {
id, operation, params, context,
requestedAt: new Date().toISOString(),
status: 'pending'
};
this.pending.set(id, request);
// Send notification to admin
await sendAdminNotification({
title: "AI Agent Approval Request",
body: "Operation: " + operation + "\nParams: " + JSON.stringify(params) + "\nContext: " + context + "",
approval_id: id
});
return { approval_id: id, status: 'pending' };
}
approve(id) {
const req = this.pending.get(id);
if (req) { req.status = 'approved'; return true; }
return false;
}
deny(id, reason) {
const req = this.pending.get(id);
if (req) { req.status = 'denied'; req.reason = reason; return true; }
return false;
}
}Step 4 — The Guardrail Wrapper
Wrap every AI tool call through the safety layer:
const rateLimiter = new RateLimiter();
const approvalQueue = new ApprovalQueue();
const auditLog = [];
async function safeExecute(operation, params, aiContext) {
// 1. Check if forbidden
if (PERMISSION_TIERS.forbidden.includes(operation)) {
auditLog.push({ operation, status: 'blocked', reason: 'forbidden' });
return { error: 'This operation is not available to AI agents.' };
}
// 2. Check rate limits for non-read operations
if (!PERMISSION_TIERS.read.includes(operation)) {
const rateCheck = rateLimiter.check(operation);
if (!rateCheck.allowed) {
auditLog.push({ operation, status: 'rate_limited' });
return { error: rateCheck.reason };
}
}
// 3. Require approval for restricted operations
if (PERMISSION_TIERS.restricted.includes(operation)) {
const approval = await approvalQueue.requestApproval(
operation, params, aiContext
);
auditLog.push({ operation, status: 'awaiting_approval', id: approval.approval_id });
return { pending: true, message: 'Awaiting human approval', ...approval };
}
// 4. Execute
const result = await executeOperation(operation, params);
auditLog.push({
operation, status: 'executed',
timestamp: new Date().toISOString(), params
});
return result;
}Step 5 — Audit Dashboard
Expose the audit log so you can review what the AI agent has been doing:
app.get('/admin/audit', (req, res) => {
const { operation, status, since } = req.query;
let logs = [...auditLog];
if (operation) logs = logs.filter(l => l.operation === operation);
if (status) logs = logs.filter(l => l.status === status);
if (since) logs = logs.filter(l => new Date(l.timestamp) > new Date(since));
res.json({
total: logs.length,
logs: logs.slice(-100) // Last 100 entries
});
});Summary
You've built a comprehensive safety layer for AI-powered token operations: permission tiers prevent dangerous calls, rate limiting stops runaway agents, human-in-the-loop approval catches high-risk operations, and audit logging provides full visibility. For more on the DUAL permission model, see Organization Roles & Permissions.