Custom Agent Integration
Integrate Agent OTP with any AI agent framework or custom implementation. Learn the patterns and best practices for secure OTP relay.
Integration Pattern
The basic pattern for integrating Agent OTP with any agent:
Agent needs verification code
│
▼
Generate key pair (once per agent session)
│
▼
Request OTP from Agent OTP
│
├─── pending_approval ──▶ Wait for user approval
│ │
│ ├─── approved ──▶ Wait for OTP
│ │ │
│ │ └─── otp_received ──▶ Consume
│ │
│ └─── denied ──▶ Handle gracefully
│
└─── expired ──▶ Handle gracefully
│
▼
Consume OTP (one-time read)
│
▼
Decrypt with private key
│
▼
Use the codeTypeScript Implementation
import {
AgentOTPClient,
generateKeyPair,
exportPublicKey,
OTPRequestStatus,
} from '@orrisai/agent-otp-sdk';
// Initialize client
const otp = new AgentOTPClient({
apiKey: process.env.AGENT_OTP_API_KEY!,
});
// Generate key pair once per agent session
const { publicKey, privateKey } = await generateKeyPair();
// Your agent's OTP-enabled action executor
class SecureAgentExecutor {
private otp: AgentOTPClient;
private publicKey: CryptoKey;
private privateKey: CryptoKey;
constructor(
otpClient: AgentOTPClient,
publicKey: CryptoKey,
privateKey: CryptoKey
) {
this.otp = otpClient;
this.publicKey = publicKey;
this.privateKey = privateKey;
}
async executeWithOTP(
action: string,
params: Record<string, unknown>
): Promise<{ success: boolean; code?: string; error?: string }> {
// 1. Request OTP
const request = await this.otp.requestOTP({
reason: `${action}: ${JSON.stringify(params)}`,
expectedSender: params.service as string,
filter: params.filter as { sources?: string[]; senderPattern?: string },
publicKey: await exportPublicKey(this.publicKey),
waitForOTP: true,
timeout: 120000,
});
// 2. Check status
if (request.status !== 'otp_received') {
return {
success: false,
error: `OTP request failed: ${request.status}`,
};
}
// 3. Consume the OTP
try {
const { code } = await this.otp.consumeOTP(request.id, this.privateKey);
return { success: true, code };
} catch (error) {
return {
success: false,
error: `Failed to consume OTP: ${error}`,
};
}
}
}
// Usage
const executor = new SecureAgentExecutor(otp, publicKey, privateKey);
const result = await executor.executeWithOTP('signup_verification', {
service: 'Acme Inc',
email: 'user@example.com',
filter: {
sources: ['email'],
senderPattern: '*@acme.com',
},
});
if (result.success) {
console.log('Received OTP:', result.code);
// Use the code for verification
}Python Implementation (Coming Soon)
from agent_otp import (
AgentOTPClient,
generate_key_pair,
export_public_key,
)
from typing import Dict, Any, Optional
class SecureAgentExecutor:
def __init__(self, api_key: str):
self.otp = AgentOTPClient(api_key=api_key)
self.public_key, self.private_key = generate_key_pair()
def execute_with_otp(
self,
action: str,
params: Dict[str, Any]
) -> Dict[str, Any]:
"""Execute an action that requires OTP verification."""
# Build filter from params
filter_opts = params.get("filter", {})
# Request OTP
request = self.otp.request_otp(
reason=f"{action}: {params}",
expected_sender=params.get("service"),
filter=filter_opts if filter_opts else None,
public_key=export_public_key(self.public_key),
wait_for_otp=True,
timeout=120
)
if request.status != "otp_received":
return {
"success": False,
"error": f"OTP request failed: {request.status}"
}
# Consume the OTP
try:
result = self.otp.consume_otp(request.id, self.private_key)
return {"success": True, "code": result.code}
except Exception as e:
return {"success": False, "error": str(e)}
# Usage
executor = SecureAgentExecutor(api_key="ak_live_xxxx")
result = executor.execute_with_otp("signup_verification", {
"service": "Acme Inc",
"email": "user@example.com",
"filter": {
"sources": ["email"],
"sender_pattern": "*@acme.com"
}
})
if result["success"]:
print(f"Received OTP: {result['code']}")Webhook Integration
For asynchronous agents, use webhooks to receive OTP notifications:
// 1. Request OTP without waiting
const request = await otp.requestOTP({
reason: 'Signup verification',
publicKey: await exportPublicKey(publicKey),
waitForOTP: false, // Don't block
webhookUrl: 'https://your-agent.com/webhooks/otp',
});
// Store request ID for later
await saveJobState({
jobId,
otpRequestId: request.id,
status: 'waiting_for_otp',
});
// 2. Handle webhook callback
app.post('/webhooks/otp', async (req, res) => {
const { request_id, status } = req.body;
// Verify webhook signature
const signature = req.headers['x-otp-signature'];
if (!verifySignature(req.body, signature)) {
return res.status(401).send('Invalid signature');
}
// Resume the job
const job = await getJobByOTPRequestId(request_id);
if (status === 'otp_received') {
// Consume the OTP
const { code } = await otp.consumeOTP(request_id, privateKey);
await completeJob(job, code);
} else if (status === 'denied' || status === 'expired') {
await failJob(job, `OTP ${status}`);
}
res.status(200).send('OK');
});Polling Pattern
If webhooks aren't available, use polling:
async function pollForOTP(
requestId: string,
maxAttempts: number = 60,
intervalMs: number = 2000
): Promise<string | null> {
for (let attempt = 0; attempt < maxAttempts; attempt++) {
const status = await otp.getOTPStatus(requestId);
switch (status.status) {
case 'otp_received':
const { code } = await otp.consumeOTP(requestId, privateKey);
return code;
case 'denied':
case 'expired':
case 'cancelled':
return null;
case 'pending_approval':
case 'approved':
// Still waiting, continue polling
await sleep(intervalMs);
break;
default:
throw new Error(`Unexpected status: ${status.status}`);
}
}
// Timeout
return null;
}
function sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}Middleware Pattern
Create middleware for your agent framework:
type Action = {
type: string;
requiresOTP?: boolean;
otpReason?: string;
params: Record<string, unknown>;
};
function createOTPMiddleware(
otp: AgentOTPClient,
publicKey: CryptoKey,
privateKey: CryptoKey
) {
return async function otpMiddleware(
action: Action,
next: (code?: string) => Promise<unknown>
) {
// Check if action requires OTP
if (!action.requiresOTP) {
return next();
}
// Request OTP
const request = await otp.requestOTP({
reason: action.otpReason || action.type,
publicKey: await exportPublicKey(publicKey),
waitForOTP: true,
});
if (request.status !== 'otp_received') {
throw new Error(`OTP request failed: ${request.status}`);
}
// Consume OTP
const { code } = await otp.consumeOTP(request.id, privateKey);
// Execute action with code
return next(code);
};
}
// Use in your agent
const middleware = createOTPMiddleware(otp, publicKey, privateKey);
await middleware(
{
type: 'signup',
requiresOTP: true,
otpReason: 'Sign up for Acme Inc',
params: { email: 'user@example.com' },
},
async (code) => {
// Use the OTP code
await completeSignup(code!);
}
);Error Handling Best Practices
import {
AgentOTPError,
AuthenticationError,
ValidationError,
RateLimitError,
OTPNotFoundError,
OTPExpiredError,
OTPAlreadyConsumedError,
OTPApprovalDeniedError,
DecryptionError,
} from '@orrisai/agent-otp-sdk';
async function executeWithOTP(action: string): Promise<{
success: boolean;
code?: string;
error?: string;
retryAfter?: number;
}> {
try {
const request = await otp.requestOTP({
reason: action,
publicKey: await exportPublicKey(publicKey),
waitForOTP: true,
});
if (request.status === 'otp_received') {
const { code } = await otp.consumeOTP(request.id, privateKey);
return { success: true, code };
}
return { success: false, error: `Status: ${request.status}` };
} catch (error) {
if (error instanceof AuthenticationError) {
// API key issue
console.error('Invalid API key');
return { success: false, error: 'Authentication failed' };
}
if (error instanceof RateLimitError) {
// Queue for retry
return {
success: false,
error: 'Rate limited',
retryAfter: error.retryAfter,
};
}
if (error instanceof ValidationError) {
// Invalid request
return { success: false, error: `Validation: ${error.message}` };
}
if (error instanceof OTPApprovalDeniedError) {
return { success: false, error: 'User denied the request' };
}
if (error instanceof OTPExpiredError) {
return { success: false, error: 'Request expired' };
}
if (error instanceof OTPAlreadyConsumedError) {
return { success: false, error: 'OTP already consumed' };
}
if (error instanceof DecryptionError) {
return { success: false, error: 'Decryption failed - check keys' };
}
// Unknown error
throw error;
}
}Testing Integration
import { AgentOTPClient, generateKeyPair, exportPublicKey } from '@orrisai/agent-otp-sdk';
describe('OTP Integration', () => {
let otp: AgentOTPClient;
let publicKey: CryptoKey;
let privateKey: CryptoKey;
beforeAll(async () => {
// Use test API key (auto-approves requests)
otp = new AgentOTPClient({
apiKey: process.env.AGENT_OTP_TEST_KEY!,
});
const keyPair = await generateKeyPair();
publicKey = keyPair.publicKey;
privateKey = keyPair.privateKey;
});
it('should request and consume OTP', async () => {
const request = await otp.requestOTP({
reason: 'Test verification',
publicKey: await exportPublicKey(publicKey),
waitForOTP: true,
});
expect(request.status).toBe('otp_received');
const { code } = await otp.consumeOTP(request.id, privateKey);
expect(code).toBeDefined();
expect(code.length).toBeGreaterThan(0);
});
it('should handle cancellation', async () => {
const request = await otp.requestOTP({
reason: 'Test cancellation',
publicKey: await exportPublicKey(publicKey),
waitForOTP: false,
});
await otp.cancelOTPRequest(request.id);
const status = await otp.getOTPStatus(request.id);
expect(status.status).toBe('cancelled');
});
});