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
Complete Implementation
src/index.ts
Copy
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
Copy
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
Copy
# .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
Copy
<!-- 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
Copy
lua chat
Copy
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
Copy
// 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:- Onfido
- Jumio
- Plaid Identity
Copy
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
})
});
Copy
const jumioApiKey = env('JUMIO_API_TOKEN');
const response = await fetch('https://netverify.com/api/v4/initiate', {
headers: {
'Authorization': `Bearer ${jumioApiKey}`,
'User-Agent': 'YourCompany/1.0.0'
}
});
Copy
const plaidKey = env('PLAID_CLIENT_ID');
const response = await fetch('https://production.plaid.com/identity/get', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
client_id: plaidKey,
secret: env('PLAID_SECRET'),
access_token: userAccessToken
})
});
Document Types Supported
Government-Issued ID
Government-Issued ID
- Driver’s License (front and back)
- State ID
- Passport
- National ID card
- Document authenticity
- Face match with selfie
- Data extraction (name, DOB, address)
- Expiration date validation
Proof of Address
Proof of Address
- Utility bill (within 3 months)
- Bank statement
- Lease agreement
- Government correspondence
- Address matches ID
- Document date within acceptable range
- Name matches applicant
Financial Documents
Financial Documents
- Bank statements
- Tax returns (for high-value accounts)
- Pay stubs (employment verification)
- Investment account statements
- Income verification
- Source of funds
- Net worth assessment
Onboarding Journey Diagram
Copy
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
Copy
// 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
Copy
// 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
Data Retention
Data Retention
- Keep applications for 7 years (regulatory requirement)
- Implement automated data deletion for rejected applications
- Archive completed applications to cold storage
Audit Logging
Audit Logging
Copy
await Data.create('audit_logs', {
action: 'document_uploaded',
applicationId: input.applicationId,
timestamp: new Date().toISOString(),
ipAddress: userIp,
userAgent: userAgent
});
Compliance Monitoring
Compliance Monitoring
- 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

