Skip to main content

Overview

Complete financial services onboarding agent with KYC (Know Your Customer) verification using Stripe Identity API for document verification and Lua Data API for application management. What it does:
  • Guide users through onboarding journey
  • Collect personal and financial information
  • Upload and verify identity documents (ID, passport)
  • Answer qualifying questions
  • Perform compliance checks
  • Create verified account
APIs used: Stripe Identity API (document verification) + Lua Data API (application tracking)

Complete Implementation

src/index.ts

import { LuaAgent, LuaSkill, LuaWebhook, PreProcessor, PostProcessor } from "lua-cli";
import {
  StartOnboardingTool,
  CollectPersonalInfoTool,
  UploadDocumentTool,
  VerifyIdentityTool,
  AnswerQualifyingQuestionsTool,
  CreateAccountTool,
  CheckOnboardingStatusTool
} from "./tools/FinancialOnboardingTools";

// Onboarding skill
const financialOnboardingSkill = new LuaSkill({
  name: "financial-onboarding",
  description: "Financial services customer onboarding with KYC verification",
  context: `
    This skill guides customers through financial account onboarding.
    
    Onboarding Flow (in order):
    1. start_onboarding: Begin new application
    2. collect_personal_info: Gather basic information
    3. upload_document: Upload ID, passport, or proof of address
    4. verify_identity: Verify uploaded documents
    5. answer_qualifying_questions: Financial suitability assessment
    6. create_account: Complete account creation
    7. check_onboarding_status: Check application status
    
    Guidelines:
    - Be professional and reassuring about data security
    - Explain why each document is needed (regulatory compliance)
    - Never rush through identity verification steps
    - Clearly communicate what happens to uploaded documents
    - Follow KYC and AML regulations
    - Ensure GDPR/CCPA compliance
  `,
  tools: [
    new StartOnboardingTool(),
    new CollectPersonalInfoTool(),
    new UploadDocumentTool(),
    new VerifyIdentityTool(),
    new AnswerQualifyingQuestionsTool(),
    new CreateAccountTool(),
    new CheckOnboardingStatusTool()
  ]
});

// Stripe Identity webhook for verification results
const stripeIdentityWebhook = new LuaWebhook({
  name: 'stripe-identity-webhook',
  description: 'Handle Stripe Identity verification events',
  secret: env('STRIPE_WEBHOOK_SECRET'),
  execute: async (event) => {
    if (event.type === 'identity.verification_session.verified') {
      const user = await User.get();
      await user.send([{
        type: 'text',
        text: '✅ Identity verification successful! Proceeding with account creation...'
      }]);
    }
    return { received: true };
  }
});

// Information validation preprocessor
const validateInformationPreProcessor = new PreProcessor({
  name: 'validate-financial-info',
  description: 'Ensure required information is provided',
  execute: async (message, user) => {
    // Ensure user has started onboarding
    const applications = await Data.search('onboarding_applications', user.email, 1);
    if (applications.count === 0) {
      return {
        block: true,
        response: "Please start the onboarding process first by providing your email address."
      };
    }
    return { block: false };
  }
});

// Compliance disclaimer postprocessor
const complianceDisclaimerPostProcessor = new PostProcessor({
  name: 'compliance-disclaimer',
  description: 'Add regulatory disclaimers to responses',
  execute: async (user, message, response, channel) => {
    return {
      modifiedResponse: response + 
        "\n\n_Banking services provided by our partner bank. FDIC insured. Member FDIC. Your information is encrypted and secure._"
    };
  }
});

// Configure agent (v3.0.0)
export const agent = new LuaAgent({
  name: "financial-onboarding-agent",
  
  persona: `You are a professional financial services onboarding specialist.
  
Your role:
- Guide customers through account opening process
- Collect required KYC information
- Verify identity documents
- Assess financial suitability
- Ensure regulatory compliance

Communication style:
- Professional and trustworthy
- Clear and reassuring
- Patient and thorough
- Transparent about data security
- Compliant with regulations

Compliance requirements:
- Follow KYC (Know Your Customer) procedures
- Adhere to AML (Anti-Money Laundering) regulations
- Ensure GDPR/CCPA compliance
- Verify identity before account creation
- Document all customer interactions

Best practices:
- Explain why each document is needed
- Reassure customers about data security
- Never rush through verification steps
- Clearly communicate processing times
- Provide next steps at each stage

Security reminders:
- All information is encrypted
- Documents are securely stored
- Compliance with banking regulations
- Data is never shared without consent`,

  
  skills: [financialOnboardingSkill],
  webhooks: [stripeIdentityWebhook],
  preProcessors: [validateInformationPreProcessor],
  postProcessors: [complianceDisclaimerPostProcessor]
});
v3.0.0 Features: This demo uses LuaAgent with webhooks for Stripe Identity events, preprocessors for validation, and postprocessors for compliance disclaimers.

src/tools/FinancialOnboardingTools.ts

import { LuaTool, Data, env } from "lua-cli";
import { z } from "zod";

// 1. Start Onboarding
export class StartOnboardingTool implements LuaTool {
  name = "start_onboarding";
  description = "Begin a new account onboarding application";
  
  inputSchema = z.object({
    email: z.string().email().describe("Applicant's email address"),
    accountType: z.enum(['individual', 'business']).describe("Type of account")
  });

  async execute(input: z.infer<typeof this.inputSchema>) {
    // Create onboarding application
    const application = await Data.create('onboarding_applications', {
      email: input.email,
      accountType: input.accountType,
      status: 'started',
      currentStep: 'personal_info',
      createdAt: new Date().toISOString(),
      completedSteps: []
    }, input.email);
    
    return {
      applicationId: application.id,
      accountType: input.accountType,
      nextStep: 'personal_info',
      message: "Application started! Let's begin by collecting your personal information.",
      estimatedTime: "5-10 minutes to complete"
    };
  }
}

// 2. Collect Personal Information
export class CollectPersonalInfoTool implements LuaTool {
  name = "collect_personal_info";
  description = "Collect applicant's personal information";
  
  inputSchema = z.object({
    applicationId: z.string(),
    personalInfo: z.object({
      firstName: z.string(),
      lastName: z.string(),
      dateOfBirth: z.string().describe("YYYY-MM-DD"),
      ssn: z.string().describe("Social Security Number (will be encrypted)"),
      phone: z.string(),
      address: z.object({
        street: z.string(),
        city: z.string(),
        state: z.string(),
        zipCode: z.string(),
        country: z.string().default('USA')
      })
    })
  });

  async execute(input: z.infer<typeof this.inputSchema>) {
    // Get application
    const app = await Data.getEntry('onboarding_applications', input.applicationId);
    
    if (!app) {
      throw new Error('Application not found');
    }
    
    // Encrypt SSN before storing (in production, use proper encryption)
    const encryptedSSN = this.encryptSSN(input.personalInfo.ssn);
    
    // Update application with personal info
    await Data.update('onboarding_applications', input.applicationId, {
      ...app.data,
      personalInfo: {
        ...input.personalInfo,
        ssn: encryptedSSN // Store encrypted
      },
      currentStep: 'document_upload',
      completedSteps: [...app.data.completedSteps, 'personal_info'],
      updatedAt: new Date().toISOString()
    });
    
    return {
      success: true,
      nextStep: 'document_upload',
      message: "Personal information saved securely. Next, please upload a government-issued ID.",
      documentsNeeded: [
        "Government-issued photo ID (driver's license or passport)",
        "Proof of address (utility bill or bank statement)"
      ]
    };
  }
  
  private encryptSSN(ssn: string): string {
    // In production, use proper encryption (AES-256, KMS, etc.)
    // This is a placeholder
    return Buffer.from(ssn).toString('base64');
  }
}

// 3. Upload Document (Stripe Identity API)
export class UploadDocumentTool implements LuaTool {
  name = "upload_document";
  description = "Upload identity verification document";
  
  inputSchema = z.object({
    applicationId: z.string(),
    documentType: z.enum(['drivers_license', 'passport', 'id_card', 'proof_of_address']),
    documentImageUrl: z.string().url().describe("URL of uploaded document image"),
    documentSide: z.enum(['front', 'back']).optional().describe("For driver's license")
  });

  async execute(input: z.infer<typeof this.inputSchema>) {
    const stripeKey = env('STRIPE_SECRET_KEY');
    
    if (!stripeKey) {
      throw new Error('Stripe API key not configured');
    }
    
    // Create Stripe Identity Verification Session
    const verificationResponse = await fetch('https://api.stripe.com/v1/identity/verification_sessions', {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${stripeKey}`,
        'Content-Type': 'application/x-www-form-urlencoded'
      },
      body: new URLSearchParams({
        'type': 'document',
        'metadata[application_id]': input.applicationId,
        'metadata[document_type]': input.documentType
      })
    });
    
    if (!verificationResponse.ok) {
      throw new Error('Failed to create verification session');
    }
    
    const verification = await verificationResponse.json();
    
    // Upload document to Stripe
    const uploadResponse = await fetch('https://files.stripe.com/v1/files', {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${stripeKey}`
      },
      body: this.createFormData(input.documentImageUrl, input.documentType)
    });
    
    const uploadedFile = await uploadResponse.json();
    
    // Save document reference in application
    const app = await Data.getEntry('onboarding_applications', input.applicationId);
    
    const documents = app.data.documents || [];
    documents.push({
      type: input.documentType,
      side: input.documentSide,
      stripeFileId: uploadedFile.id,
      stripeVerificationId: verification.id,
      uploadedAt: new Date().toISOString(),
      status: 'pending_verification'
    });
    
    await Data.update('onboarding_applications', input.applicationId, {
      ...app.data,
      documents,
      currentStep: 'identity_verification',
      updatedAt: new Date().toISOString()
    });
    
    return {
      success: true,
      documentId: uploadedFile.id,
      verificationId: verification.id,
      status: 'uploaded',
      message: "Document uploaded successfully. Verification in progress...",
      nextStep: "We'll verify your identity. This usually takes 1-2 minutes.",
      verificationUrl: verification.url // User can complete verification here
    };
  }
  
  private createFormData(imageUrl: string, documentType: string): FormData {
    const formData = new FormData();
    formData.append('purpose', 'identity_document');
    formData.append('file', imageUrl);
    return formData;
  }
}

// 4. Verify Identity (Check Stripe Identity Results)
export class VerifyIdentityTool implements LuaTool {
  name = "verify_identity";
  description = "Check identity verification status";
  
  inputSchema = z.object({
    applicationId: z.string()
  });

  async execute(input: z.infer<typeof this.inputSchema>) {
    const stripeKey = env('STRIPE_SECRET_KEY');
    const app = await Data.getEntry('onboarding_applications', input.applicationId);
    
    if (!app.data.documents || app.data.documents.length === 0) {
      return {
        verified: false,
        message: "No documents uploaded yet. Please upload your ID first."
      };
    }
    
    // Check verification status with Stripe
    const latestDoc = app.data.documents[app.data.documents.length - 1];
    
    const response = await fetch(
      `https://api.stripe.com/v1/identity/verification_sessions/${latestDoc.stripeVerificationId}`,
      {
        headers: { 'Authorization': `Bearer ${stripeKey}` }
      }
    );
    
    const verification = await response.json();
    
    const isVerified = verification.status === 'verified';
    
    // Update application
    if (isVerified) {
      await Data.update('onboarding_applications', input.applicationId, {
        ...app.data,
        identityVerified: true,
        verificationResult: {
          verified: true,
          verifiedAt: new Date().toISOString(),
          documentType: verification.last_verification_report?.document?.type,
          nameMatch: verification.last_verification_report?.id_number?.status === 'verified'
        },
        currentStep: 'qualifying_questions',
        completedSteps: [...app.data.completedSteps, 'identity_verification']
      });
    }
    
    return {
      verified: isVerified,
      status: verification.status,
      message: isVerified 
        ? "✅ Identity verified successfully! Let's continue with some qualifying questions."
        : verification.status === 'processing'
        ? "⏳ Verification in progress. Please wait..."
        : "❌ Verification failed. Please upload a clearer image of your ID.",
      nextStep: isVerified ? 'qualifying_questions' : 'document_upload',
      verificationDetails: isVerified ? {
        documentType: verification.last_verification_report?.document?.type,
        issueDate: verification.last_verification_report?.document?.issued_date,
        expirationDate: verification.last_verification_report?.document?.expiration_date
      } : null
    };
  }
}

// 5. Answer Qualifying Questions
export class AnswerQualifyingQuestionsTool implements LuaTool {
  name = "answer_qualifying_questions";
  description = "Complete financial suitability questionnaire";
  
  inputSchema = z.object({
    applicationId: z.string(),
    answers: z.object({
      annualIncome: z.enum(['under_25k', '25k_50k', '50k_100k', '100k_250k', 'over_250k']),
      employmentStatus: z.enum(['employed', 'self_employed', 'unemployed', 'retired', 'student']),
      investmentExperience: z.enum(['none', 'limited', 'moderate', 'extensive']),
      riskTolerance: z.enum(['conservative', 'moderate', 'aggressive']),
      investmentGoals: z.array(z.enum(['retirement', 'wealth_building', 'income', 'preservation'])),
      investmentHorizon: z.enum(['short_term', 'medium_term', 'long_term']),
      liquidNetWorth: z.enum(['under_10k', '10k_50k', '50k_100k', '100k_500k', 'over_500k']),
      sourceOfFunds: z.enum(['employment', 'business', 'investments', 'inheritance', 'other'])
    })
  });

  async execute(input: z.infer<typeof this.inputSchema>) {
    const app = await Data.getEntry('onboarding_applications', input.applicationId);
    
    // Calculate suitability score
    const suitabilityScore = this.calculateSuitability(input.answers);
    
    // Determine if applicant qualifies
    const qualifies = suitabilityScore.score >= 60;
    
    // Update application
    await Data.update('onboarding_applications', input.applicationId, {
      ...app.data,
      qualifyingAnswers: input.answers,
      suitabilityScore: suitabilityScore,
      qualifies,
      currentStep: qualifies ? 'account_creation' : 'under_review',
      completedSteps: [...app.data.completedSteps, 'qualifying_questions'],
      updatedAt: new Date().toISOString()
    });
    
    if (!qualifies) {
      return {
        success: false,
        qualifies: false,
        score: suitabilityScore.score,
        message: "Thank you for your application. Based on your responses, we need to review your application manually. Our team will contact you within 2 business days.",
        nextSteps: "Our compliance team will review your application"
      };
    }
    
    return {
      success: true,
      qualifies: true,
      score: suitabilityScore.score,
      riskProfile: suitabilityScore.riskProfile,
      recommendedProducts: this.getRecommendedProducts(input.answers),
      message: "Great! You qualify for an account. Let's create your account now.",
      nextStep: 'account_creation'
    };
  }
  
  private calculateSuitability(answers: any) {
    let score = 0;
    
    // Income scoring
    const incomeScores = {
      'under_25k': 10,
      '25k_50k': 20,
      '50k_100k': 30,
      '100k_250k': 40,
      'over_250k': 50
    };
    score += incomeScores[answers.annualIncome] || 0;
    
    // Experience scoring
    const experienceScores = {
      'none': 5,
      'limited': 15,
      'moderate': 25,
      'extensive': 35
    };
    score += experienceScores[answers.investmentExperience] || 0;
    
    // Net worth scoring
    const netWorthScores = {
      'under_10k': 5,
      '10k_50k': 10,
      '50k_100k': 15,
      '100k_500k': 20,
      'over_500k': 25
    };
    score += netWorthScores[answers.liquidNetWorth] || 0;
    
    // Determine risk profile
    let riskProfile = 'conservative';
    if (answers.riskTolerance === 'aggressive' && answers.investmentHorizon === 'long_term') {
      riskProfile = 'aggressive';
    } else if (answers.riskTolerance === 'moderate') {
      riskProfile = 'moderate';
    }
    
    return {
      score,
      riskProfile,
      passedCompliance: score >= 60
    };
  }
  
  private getRecommendedProducts(answers: any) {
    const products = [];
    
    if (answers.investmentGoals.includes('retirement')) {
      products.push('IRA Account', '401(k) Rollover');
    }
    
    if (answers.riskTolerance === 'conservative') {
      products.push('Money Market Account', 'CD Account');
    } else if (answers.riskTolerance === 'aggressive') {
      products.push('Investment Account', 'Options Trading');
    } else {
      products.push('Savings Account', 'Investment Account');
    }
    
    return products;
  }
}

// 6. Create Account
export class CreateAccountTool implements LuaTool {
  name = "create_account";
  description = "Create verified financial services account";
  
  inputSchema = z.object({
    applicationId: z.string(),
    accountProducts: z.array(z.string()).describe("Selected account products"),
    agreeToTerms: z.boolean().describe("Must accept terms and conditions"),
    agreeToPrivacyPolicy: z.boolean()
  });

  async execute(input: z.infer<typeof this.inputSchema>) {
    if (!input.agreeToTerms || !input.agreeToPrivacyPolicy) {
      throw new Error('You must agree to the terms and conditions to create an account');
    }
    
    const app = await Data.getEntry('onboarding_applications', input.applicationId);
    
    // Verify all steps completed
    if (!app.data.identityVerified) {
      throw new Error('Identity verification must be completed first');
    }
    
    if (!app.data.qualifies) {
      throw new Error('Application is pending review');
    }
    
    // Create account in your banking system (external API)
    const bankingApiKey = env('BANKING_API_KEY');
    
    const accountResponse = await fetch('https://your-banking-api.com/api/accounts', {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${bankingApiKey}`,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        customer: {
          first_name: app.data.personalInfo.firstName,
          last_name: app.data.personalInfo.lastName,
          email: app.data.email,
          date_of_birth: app.data.personalInfo.dateOfBirth,
          ssn: app.data.personalInfo.ssn, // Encrypted
          address: app.data.personalInfo.address,
          phone: app.data.personalInfo.phone
        },
        products: input.accountProducts,
        verification: {
          identity_verified: true,
          verification_id: app.data.verificationResult.verificationId,
          kyc_status: 'approved'
        },
        suitability: app.data.suitabilityScore,
        metadata: {
          application_id: input.applicationId,
          onboarding_source: 'ai_agent'
        }
      })
    });
    
    if (!accountResponse.ok) {
      throw new Error('Failed to create account. Please contact support.');
    }
    
    const account = await accountResponse.json();
    
    // Update application as completed
    await Data.update('onboarding_applications', input.applicationId, {
      ...app.data,
      status: 'completed',
      accountId: account.account_id,
      accountNumber: account.account_number,
      products: input.accountProducts,
      completedSteps: [...app.data.completedSteps, 'account_creation'],
      completedAt: new Date().toISOString()
    });
    
    return {
      success: true,
      accountId: account.account_id,
      accountNumber: account.account_number.replace(/\d(?=\d{4})/g, '*'), // Mask all but last 4
      products: input.accountProducts,
      loginUrl: account.login_url,
      temporaryPassword: account.temporary_password,
      message: `🎉 Account created successfully! Your account number is ${account.account_number.slice(-4)}. Check your email for login credentials.`,
      nextSteps: [
        "Check your email for account details",
        "Set up online banking at " + account.login_url,
        "Fund your account to start using services",
        "Download our mobile app for easy access"
      ]
    };
  }
}

// 7. Check Onboarding Status
export class CheckOnboardingStatusTool implements LuaTool {
  name = "check_onboarding_status";
  description = "Check the status of an onboarding application";
  
  inputSchema = z.object({
    applicationId: z.string(),
    email: z.string().email().describe("Email for verification")
  });

  async execute(input: z.infer<typeof this.inputSchema>) {
    const app = await Data.getEntry('onboarding_applications', input.applicationId);
    
    if (!app || app.data.email !== input.email) {
      throw new Error('Application not found or email mismatch');
    }
    
    const stepStatus = {
      started: '🟢 Started',
      personal_info: app.data.completedSteps.includes('personal_info') ? '✅ Complete' : '⏳ Pending',
      document_upload: app.data.documents?.length > 0 ? '✅ Complete' : '⏳ Pending',
      identity_verification: app.data.identityVerified ? '✅ Verified' : '⏳ Pending',
      qualifying_questions: app.data.qualifyingAnswers ? '✅ Complete' : '⏳ Pending',
      account_creation: app.data.accountId ? '✅ Complete' : '⏳ Pending'
    };
    
    return {
      applicationId: input.applicationId,
      status: app.data.status,
      currentStep: app.data.currentStep,
      progress: stepStatus,
      completedSteps: app.data.completedSteps,
      nextStep: this.getNextStepMessage(app.data.currentStep),
      estimatedCompletion: app.data.status === 'completed' 
        ? 'Completed'
        : this.calculateEstimatedCompletion(app.data.completedSteps.length)
    };
  }
  
  private getNextStepMessage(currentStep: string): string {
    const messages = {
      personal_info: "Please provide your personal information",
      document_upload: "Please upload your government-issued ID",
      identity_verification: "Verifying your identity...",
      qualifying_questions: "Please answer the qualifying questions",
      account_creation: "Ready to create your account!",
      under_review: "Application under manual review",
      completed: "Application complete!"
    };
    return messages[currentStep] || "Continue with onboarding";
  }
  
  private calculateEstimatedCompletion(completedSteps: number): string {
    const totalSteps = 5;
    const remaining = totalSteps - completedSteps;
    return `${remaining * 2} minutes`;
  }
}

Environment Setup

# .env
STRIPE_SECRET_KEY=sk_test_your_stripe_key
BANKING_API_KEY=your_banking_api_key
BANKING_API_URL=https://your-banking-api.com

Document Upload Flow

Frontend Integration

<!-- File upload form -->
<form id="document-upload">
  <input type="file" id="id-front" accept="image/*,application/pdf" />
  <input type="file" id="id-back" accept="image/*,application/pdf" />
  <button type="submit">Upload Documents</button>
</form>

<script>
document.getElementById('document-upload').addEventListener('submit', async (e) => {
  e.preventDefault();
  
  // Upload to your server/CDN first
  const formData = new FormData();
  formData.append('front', document.getElementById('id-front').files[0]);
  formData.append('back', document.getElementById('id-back').files[0]);
  
  const upload = await fetch('/api/upload-documents', {
    method: 'POST',
    body: formData
  });
  
  const { frontUrl, backUrl } = await upload.json();
  
  // Then pass URLs to AI agent
  // The agent will call upload_document tool with these URLs
});
</script>

Testing Conversation Flow

lua chat
Select sandbox mode, then test this example conversation:
User: "I want to open an investment account"
AI: [Calls start_onboarding]
    "Great! Let's get you started. What's your email address?"

User: "[email protected]"
AI: [Calls collect_personal_info]
    "Perfect! I'll need some personal information. What's your full name?"

User: "John Doe, DOB 1990-01-15, SSN 123-45-6789, address: 123 Main St..."
AI: [Saves info]
    "Information saved securely. Now I need to verify your identity. 
     Please upload a photo of your driver's license or passport."

User: [Uploads document images]
AI: [Calls upload_document, then verify_identity]
    "Document uploaded! Verifying your identity... ✅ Identity verified! 
     Now, let's answer some questions about your financial goals..."

User: "I make $75k/year, moderate experience, looking for long-term retirement..."
AI: [Calls answer_qualifying_questions]
    "Based on your profile, you qualify! I recommend an IRA Account 
     and Investment Account. Shall we create your account?"

User: "Yes, create it"
AI: [Calls create_account]
    "🎉 Account created! Your account number is ****5678. 
     Check your email for login details."

Key Features

Stripe Identity

Document verification API

Lua Data

Application state management

KYC Compliant

Regulatory compliance built-in

Multi-Step Journey

Guided onboarding flow

Risk Assessment

Suitability scoring

Secure

Encrypted PII storage

Compliance & Security

Regulatory Compliance RequiredThis demo shows technical implementation. For production:
  • ✅ Implement proper encryption for PII (use KMS, not base64)
  • ✅ Follow KYC/AML regulations (Bank Secrecy Act, Patriot Act)
  • ✅ Maintain audit logs of all data access
  • ✅ Use HTTPS only
  • ✅ Implement data retention policies
  • ✅ Follow GDPR/CCPA for data privacy
  • ✅ Store documents in compliant storage (encrypted at rest)
  • ✅ Conduct regular security audits
  • ✅ Implement fraud detection
  • ✅ Follow FinCEN guidelines

Security Best Practices

// Encrypt sensitive data before storage
import crypto from 'crypto';

function encryptPII(data: string): string {
  const algorithm = 'aes-256-gcm';
  const key = Buffer.from(env('ENCRYPTION_KEY'), 'hex');
  const iv = crypto.randomBytes(16);
  
  const cipher = crypto.createCipheriv(algorithm, key, iv);
  let encrypted = cipher.update(data, 'utf8', 'hex');
  encrypted += cipher.final('hex');
  
  const authTag = cipher.getAuthTag();
  
  return JSON.stringify({
    encrypted,
    iv: iv.toString('hex'),
    authTag: authTag.toString('hex')
  });
}

// Use in your tools
const encryptedSSN = encryptPII(input.personalInfo.ssn);

Alternative Document Verification Services

This demo uses Stripe Identity, but you can swap with:
const onfidoApiKey = env('ONFIDO_API_KEY');

const response = await fetch('https://api.onfido.com/v3/applicants', {
  method: 'POST',
  headers: {
    'Authorization': `Token token=${onfidoApiKey}`,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    first_name: input.firstName,
    last_name: input.lastName,
    email: input.email
  })
});

Document Types Supported

  • Driver’s License (front and back)
  • State ID
  • Passport
  • National ID card
Verification checks:
  • Document authenticity
  • Face match with selfie
  • Data extraction (name, DOB, address)
  • Expiration date validation
  • Utility bill (within 3 months)
  • Bank statement
  • Lease agreement
  • Government correspondence
Verification checks:
  • Address matches ID
  • Document date within acceptable range
  • Name matches applicant
  • Bank statements
  • Tax returns (for high-value accounts)
  • Pay stubs (employment verification)
  • Investment account statements
Used for:
  • Income verification
  • Source of funds
  • Net worth assessment

Onboarding Journey Diagram

1. Start Application

2. Collect Personal Info

3. Upload Documents (ID + Proof of Address)

4. Identity Verification (Stripe Identity API)

5. Qualifying Questions (Risk Assessment)

6. Suitability Check

7. Create Account (if qualified)
   ✅ Account Active

Customization

Add Additional Verification

// Add selfie verification
export class CaptureSelfie Tool extends LuaTool {
  async execute(input: { applicationId: string, selfieUrl: string }) {
    // Upload selfie to Stripe
    const response = await fetch('https://api.stripe.com/v1/identity/verification_sessions', {
      method: 'POST',
      body: new URLSearchParams({
        type: 'selfie',
        metadata: { application_id: input.applicationId }
      })
    });
    
    // Stripe compares selfie with ID photo
    return { verified: true };
  }
}

Add Fraud Checks

// Integrate with fraud detection service
const fraudCheck = await fetch('https://fraud-api.com/check', {
  method: 'POST',
  body: JSON.stringify({
    email: app.data.email,
    ip_address: userIp,
    device_fingerprint: deviceId
  })
});

if (fraudCheck.risk_score > 0.7) {
  // Flag for manual review
  await Data.update(applicationId, {
    ...app.data,
    status: 'fraud_review',
    flaggedForReview: true
  });
}

Key Takeaways

Multi-Step Flow

Guided journey with state management

External + Platform

Stripe Identity + Lua Data

Compliance Ready

KYC/AML patterns shown

Secure by Design

Encryption and security practices

Production Considerations

  • Keep applications for 7 years (regulatory requirement)
  • Implement automated data deletion for rejected applications
  • Archive completed applications to cold storage
await Data.create('audit_logs', {
  action: 'document_uploaded',
  applicationId: input.applicationId,
  timestamp: new Date().toISOString(),
  ipAddress: userIp,
  userAgent: userAgent
});
  • Regular review of declined applications
  • Monthly compliance reports
  • Suspicious activity reporting (SAR)
  • Customer due diligence (CDD)

Next Demo

HR Assistant

See internal employee management with BambooHR