Magic Upgrade Flow
How AI agents can request tier upgrades from operators without breaking their workflow
The Challenge
AI agents often discover they need additional capabilities mid-task:
- Trying to send an email but on free tier
- Hitting daily send limits
- Needing custom domains
- Requiring SMTP credentials
Traditional APIs would just return an error. The agent fails, the task fails, and a human has to intervene, debug, and manually upgrade.
The Magic Upgrade Solution
daimon.email provides a seamless upgrade flow that keeps agents running:
- Agent hits a limit → API returns a rich error with upgrade context
- Agent generates magic link → One-click upgrade URL for the operator
- Agent provides script → Tells operator exactly what to say/do
- Operator clicks link → No login required, straight to Stripe checkout
- Webhook fires → Agent is notified within 5 seconds of upgrade
- Agent continues → Automatically retries the original operation
Info
Magic links are cryptographically signed URLs that allow upgrade without authentication. They expire after 7 days and are single-use. The operator never needs to log in or remember credentials.
Complete Implementation
Step 1: Agent Hits a Limit
When an agent tries an operation beyond its tier, it receives a structured error response:
try {
await client.inboxes.send(inboxId, {
to: 'customer@example.com',
subject: 'Your order is ready',
body: 'Click here to track your shipment...'
});
} catch (error) {
if (error.code === 'SEND_REQUIRES_PAID') {
console.log('Error details:', {
code: error.code,
message: error.message,
upgradeContext: error.upgradeContext
});
}
}try:
client.inboxes.send(inbox_id, {
'to': 'customer@example.com',
'subject': 'Your order is ready',
'body': 'Click here to track your shipment...'
})
except DaimonError as e:
if e.code == 'SEND_REQUIRES_PAID':
print('Error details:', {
'code': e.code,
'message': e.message,
'upgrade_context': e.upgrade_context
})Response
{
"error": "SEND_REQUIRES_PAID",
"message": "Sending emails requires a paid tier",
"upgrade_context": {
"operator_action_url": "https://daimon.email/upgrade?token=eyJhbGc...",
"operator_action_label": "Add a payment method to enable sending",
"agent_script": "Tell your operator: I need sending access to complete the customer notification. Here's a direct upgrade link: https://daimon.email/upgrade?token=eyJhbGc..."
}
}Step 2: Agent Communicates with Operator
The agent uses the agent_script to communicate the need clearly:
class EmailAgent {
async handleUpgradeNeeded(error) {
// Option 1: Log for operator monitoring
console.log(`🚨 OPERATOR ACTION NEEDED: ${error.upgradeContext.agent_script}`);
// Option 2: Send to operator communication channel
await this.notifyOperator({
urgency: 'medium',
message: error.upgradeContext.agent_script,
action_url: error.upgradeContext.operator_action_url,
action_label: error.upgradeContext.operator_action_label
});
// Option 3: If agent has UI integration
await this.ui.showAlert({
type: 'upgrade_required',
message: error.upgradeContext.agent_script,
button: {
label: error.upgradeContext.operator_action_label,
url: error.upgradeContext.operator_action_url
}
});
// Wait for upgrade to complete
await this.waitForUpgrade();
}
}class EmailAgent:
async def handle_upgrade_needed(self, error):
# Option 1: Log for operator monitoring
print(f"🚨 OPERATOR ACTION NEEDED: {error.upgrade_context['agent_script']}")
# Option 2: Send to operator communication channel
await self.notify_operator({
'urgency': 'medium',
'message': error.upgrade_context['agent_script'],
'action_url': error.upgrade_context['operator_action_url'],
'action_label': error.upgrade_context['operator_action_label']
})
# Option 3: If agent has UI integration
await self.ui.show_alert({
'type': 'upgrade_required',
'message': error.upgrade_context['agent_script'],
'button': {
'label': error.upgrade_context['operator_action_label'],
'url': error.upgrade_context['operator_action_url']
}
})
# Wait for upgrade to complete
await self.wait_for_upgrade()Step 3: Generate Fresh Magic Links
Magic links in error responses are pre-generated. For custom flows, agents can generate fresh links:
// Generate a new magic upgrade link
const link = await client.account.createUpgradeLink({
target_tier: 'developer', // or 'growth'
metadata: {
agent_id: 'agent-7392',
task: 'customer-notification',
urgency: 'high'
}
});
console.log('Generated upgrade URL:', link.url);
console.log('Expires at:', link.expires_at);
// Share with operator
await sendToSlack(`
I need to upgrade to continue sending customer notifications.
Current task blocked: Order confirmation emails
Required tier: Developer ($9/month)
→ ${link.url}
This link expires in 7 days.
`);# Generate a new magic upgrade link
link = client.account.create_upgrade_link({
'target_tier': 'developer', # or 'growth'
'metadata': {
'agent_id': 'agent-7392',
'task': 'customer-notification',
'urgency': 'high'
}
})
print(f"Generated upgrade URL: {link['url']}")
print(f"Expires at: {link['expires_at']}")
# Share with operator
send_to_slack(f"""
I need to upgrade to continue sending customer notifications.
Current task blocked: Order confirmation emails
Required tier: Developer ($9/month)
→ {link['url']}
This link expires in 7 days.
""")Step 4: Operator Clicks the Magic Link
When the operator clicks the link, they see a simple upgrade page:
- No login required — The JWT in the URL identifies the account
- Clear context — Shows why the agent needs the upgrade
- One-click checkout — Stripe Checkout handles payment
- Instant activation — Account upgrades immediately upon payment
The upgrade page shows:
- Agent that requested the upgrade
- Current tier vs requested tier
- Features that will be unlocked
- Monthly price
- "Upgrade Now" button → Stripe Checkout
Step 5: Agent Receives Webhook Notification
Within 5 seconds of payment, a webhook fires to notify the agent:
// Webhook handler
app.post('/webhook', async (req, res) => {
const signature = req.headers['x-daimon-signature'];
if (!verifyWebhookSignature(req.body, signature, WEBHOOK_SECRET)) {
return res.status(401).send('Invalid signature');
}
const { event, data } = req.body;
if (event === 'account.upgraded') {
console.log('🎉 Account upgraded!', {
old_tier: data.old_tier,
new_tier: data.new_tier,
upgraded_at: data.upgraded_at,
expires_at: data.expires_at
});
// Resume blocked operations
await this.resumeBlockedTasks();
}
res.status(200).send('OK');
});
// In the agent's task queue
async resumeBlockedTasks() {
const blocked = await this.getBlockedTasks();
for (const task of blocked) {
if (task.blocked_reason === 'SEND_REQUIRES_PAID') {
// Retry the send operation
await this.retrySend(task);
}
}
}# Webhook handler
@app.post('/webhook')
async def handle_webhook(request):
signature = request.headers.get('X-Daimon-Signature')
if not verify_webhook_signature(request.body, signature, WEBHOOK_SECRET):
return Response(status=401, text='Invalid signature')
data = request.json()
if data['event'] == 'account.upgraded':
print('🎉 Account upgraded!', {
'old_tier': data['data']['old_tier'],
'new_tier': data['data']['new_tier'],
'upgraded_at': data['data']['upgraded_at'],
'expires_at': data['data']['expires_at']
})
# Resume blocked operations
await resume_blocked_tasks()
return Response(status=200, text='OK')
# In the agent's task queue
async def resume_blocked_tasks():
blocked = await get_blocked_tasks()
for task in blocked:
if task['blocked_reason'] == 'SEND_REQUIRES_PAID':
# Retry the send operation
await retry_send(task)All Upgrade Trigger Codes
Your agent should handle these upgrade scenarios:
| Error Code | Description | Required Tier | Monthly Cost |
|---|---|---|---|
SEND_REQUIRES_PAID | Sending any email | Developer | $9 |
SEND_LIMIT_EXCEEDED | Daily send limit hit | Growth | $49 |
DOMAIN_REQUIRES_PAID | Custom domain setup | Developer | $9 |
INBOX_LIMIT_EXCEEDED | Too many inboxes | Growth | $49 |
SMTP_REQUIRES_PAID | SMTP credentials | Developer | $9 |
WEBHOOK_ENDPOINT_UNHEALTHY | Webhook failing | Any | - |
ACCOUNT_UNDER_REVIEW | Suspicious activity | Contact support | - |
Advanced Patterns
Pattern: Graceful Degradation
Operate within current tier limits while waiting for upgrade:
class AdaptiveEmailAgent {
async sendEmail(to, subject, body) {
const caps = await this.getCapabilities();
if (caps.can_send) {
// Send directly
return await this.client.send(to, subject, body);
} else if (caps.can_create_draft) {
// Create draft for human to send later
const draft = await this.client.drafts.create({
to, subject, body,
note: 'Auto-draft: Pending tier upgrade for sending'
});
await this.notifyOperator({
message: `Created draft "${subject}" - upgrade required to send`,
upgrade_url: caps.upgrade_url
});
return { status: 'drafted', draft_id: draft.id };
} else {
// Store locally until upgraded
await this.queueForLater({ to, subject, body });
await this.requestUpgrade();
return { status: 'queued' };
}
}
}Pattern: Proactive Upgrade Requests
Monitor usage and request upgrades before hitting limits:
class ProactiveAgent {
async checkUpgradeNeeded() {
const metrics = await this.client.getMetrics();
// At 80% of daily limit
if (metrics.sends_today / metrics.daily_limit > 0.8) {
const link = await this.client.account.createUpgradeLink({
target_tier: 'growth',
metadata: {
reason: 'Approaching daily send limit',
current_usage: metrics.sends_today,
limit: metrics.daily_limit
}
});
await this.notifyOperator({
urgency: 'low',
message: `📊 I'm at 80% of my daily send limit. Consider upgrading to Growth tier for 10x more sends: ${link.url}`
});
}
}
}Pattern: Upgrade Analytics
Track upgrade requests and success rates:
class AnalyticsAgent {
async trackUpgradeRequest(reason, tier) {
await this.analytics.track('upgrade_requested', {
reason,
requested_tier: tier,
current_tier: this.currentTier,
timestamp: new Date().toISOString()
});
}
async trackUpgradeComplete(oldTier, newTier) {
await this.analytics.track('upgrade_completed', {
old_tier: oldTier,
new_tier: newTier,
time_to_upgrade: Date.now() - this.upgradeRequestTime,
blocked_tasks_resumed: this.blockedTasks.length
});
}
}Implementation Checklist
When implementing magic upgrade flows in your agent:
- Handle all upgrade error codes appropriately
- Store blocked tasks for retry after upgrade
- Register webhook endpoint for
account.upgradedevents - Implement operator notification channel (Slack, Discord, logs)
- Add upgrade request tracking/analytics
- Test graceful degradation within tier limits
- Implement proactive upgrade monitoring
- Handle magic link expiration (7 days)
- Verify webhook signatures for security
Operator Experience
From the operator's perspective, the flow is seamless:
- Notification appears in Slack/Discord/logs
- Context is clear — why the agent needs upgrade
- One click on the magic link
- No login — goes straight to checkout
- Payment via Stripe — saved cards, Apple Pay, etc.
- Instant confirmation — agent resumes immediately
Example operator notification:
🤖 Sales Agent needs upgrade
I've queued 47 outreach emails but need sending access to deliver them.
Current tier: Free (receive only)
Required tier: Developer ($9/month)
Blocked tasks: 47 emails
→ Upgrade now: https://daimon.email/upgrade?token=eyJ...
(Link expires in 7 days)
Once upgraded, I'll automatically send all queued emails.Security Considerations
Magic Link Security
- Signed JWTs: Links contain cryptographically signed tokens
- Time-limited: Expire after 7 days
- Single-use: Can only be used once
- Account-bound: Only upgrade the specific account
- No PII: Token doesn't contain sensitive information
Webhook Verification
Always verify webhook signatures to ensure upgrades are legitimate:
function verifyWebhookSignature(payload, signature, secret) {
const hmac = crypto.createHmac('sha256', secret);
hmac.update(JSON.stringify(payload));
const calculated = `sha256=${hmac.digest('hex')}`;
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(calculated)
);
}Comparison: Magic Links vs Traditional Upgrades
| Aspect | Magic Link (daimon.email) | Traditional |
|---|---|---|
| Authentication | None required | Email + password |
| Time to upgrade | <30 seconds | 5-10 minutes |
| Context | Embedded in link | Lost, operator must investigate |
| Operator effort | Single click | Multiple steps |
| Agent awareness | Instant webhook | Must poll or wait |
| Mobile friendly | Yes | Often difficult |
| Expiration | 7 days | Session timeout |