Skip to main content
The Drift backend is an Express.js API built with TypeScript that orchestrates simulations, aggregates financial data, and integrates with external services.

Technology Stack

TechnologyVersionPurpose
Express4.18.2Web framework
TypeScript5.3.0Type safety
tsx4.7.0TypeScript execution
Zod3.22.0Runtime validation
Axios1.6.0HTTP client
Plaid41.1.0Bank account API
@google/generative-ai0.21.0Gemini AI SDK
ElevenLabs1.59.0Voice synthesis
OpenAI4.24.0GPT integration

Project Structure

apps/api/
├── src/
   ├── index.ts              # Express app entry point
   ├── routes/              # API route handlers
   ├── simulation.ts    # Monte Carlo endpoints
   ├── plaid.ts        # Plaid integration
   ├── nessie.ts       # Nessie bank API
   ├── ai.ts           # AI services
   ├── llm.ts          # LLM endpoints
   ├── whatif.ts       # What-if analysis
   └── jobs.ts         # HPC cluster jobs
   ├── services/           # Business logic
   ├── simulationService.ts
   ├── plaidService.ts
   ├── nessieService.ts
   ├── geminiService.ts
   ├── elevenLabsService.ts
   ├── llmService.ts
   ├── clusterService.ts
   ├── whatIfService.ts
   └── accountMappers.ts
   ├── types/              # TypeScript types
   └── index.ts
   └── utils/              # Utilities
       └── index.ts
├── package.json
└── tsconfig.json

Application Entry Point

Location: src/index.ts
import express from 'express'
import cors from 'cors'
import dotenv from 'dotenv'
import nessieRouter from './routes/nessie.js'
import simulationRouter from './routes/simulation.js'
import llmRouter from './routes/llm.js'
import whatIfRouter from './routes/whatif.js'
import aiRouter from './routes/ai.js'
import jobsRouter from './routes/jobs.js'
import plaidRouter from './routes/plaid.js'

dotenv.config()

const app = express()
const PORT = process.env.PORT || 3001

// Middleware
app.use(cors())
app.use(express.json({ limit: '50mb' })) // Increased for audio uploads

// Health check
app.get('/health', (req, res) => {
  res.json({ status: 'ok', timestamp: new Date().toISOString() })
})

// Routes
app.use('/api/nessie', nessieRouter)
app.use('/api', simulationRouter)
app.use('/api', llmRouter)
app.use('/api', whatIfRouter)
app.use('/api/ai', aiRouter)
app.use('/api', jobsRouter)
app.use('/api/plaid', plaidRouter)

// Error handler
app.use((err: Error, req, res, next) => {
  console.error(`[${req.method} ${req.path}] Error:`, err.message)
  res.status(500).json({ error: 'Internal server error' })
})

app.listen(PORT, () => {
  console.log(`API server running on http://localhost:${PORT}`)
})
The API uses a 50MB JSON body limit to support audio file uploads for voice input.

Routes

Simulation Routes

Location: src/routes/simulation.ts

GET /api/financial-profile

Aggregates financial profile from Nessie accounts. Query Parameters:
  • customerId (required): Nessie customer ID
Response:
{
  "liquidAssets": 45000,
  "creditDebt": 3500,
  "loanDebt": 25000,
  "monthlyLoanPayments": 450,
  "monthlyIncome": 6500,
  "monthlyBills": 1200,
  "monthlySpending": 4200,
  "spendingByCategory": {
    "Groceries": 600,
    "Restaurants": 400,
    "Transportation": 300
  },
  "spendingVolatility": 0.18
}
Implementation:
router.get('/financial-profile', async (req, res) => {
  const customerId = req.query.customerId as string
  if (!customerId) {
    return res.status(400).json({ error: 'customerId required' })
  }

  const accounts = await nessieService.getAccounts(customerId)
  
  // Calculate liquid assets (checking + savings)
  const liquidAssets = accounts
    .filter(a => a.type === 'Checking' || a.type === 'Savings')
    .reduce((sum, a) => sum + a.balance, 0)

  // Calculate credit debt
  const creditDebt = accounts
    .filter(a => a.type === 'Credit Card')
    .reduce((sum, a) => sum + a.balance, 0)

  // ... more aggregation logic

  res.json({ liquidAssets, creditDebt, /* ... */ })
})

POST /api/simulate

Runs Monte Carlo simulation. Request Body:
{
  "financialProfile": {
    "liquidAssets": 45000,
    "creditDebt": 3500,
    "loanDebt": 25000,
    "monthlyLoanPayments": 450,
    "monthlyIncome": 6500,
    "monthlySpending": 4200,
    "spendingByCategory": {},
    "spendingVolatility": 0.15
  },
  "userInputs": {
    "monthlyIncome": 6500,
    "age": 32,
    "riskTolerance": "medium"
  },
  "goal": {
    "targetAmount": 50000,
    "timelineMonths": 36,
    "goalType": "savings"
  },
  "simulationParams": {
    "nSimulations": 100000
  }
}
Response:
{
  "successProbability": 0.73,
  "medianOutcome": 52340,
  "percentiles": {
    "p10": 38200,
    "p25": 45100,
    "p50": 52340,
    "p75": 59800,
    "p90": 67200
  },
  "mean": 52500,
  "std": 12400,
  "worstCase": 15600,
  "bestCase": 92300,
  "assumptions": { /* ... */ }
}
Implementation:
router.post('/simulate', async (req, res) => {
  try {
    const request: SimulationRequest = req.body
    const results = await simulationService.runSimulation(request)
    res.json(results)
  } catch (error) {
    console.error('Error running simulation:', error)
    res.status(500).json({ error: 'Failed to run simulation' })
  }
})

POST /api/simulate-enhanced

Runs simulation with Plaid-derived enhanced parameters. Features:
  • Fetches real account data from Plaid
  • Derives investment returns from actual portfolio allocation
  • Per-card credit interest modeling
  • Loan amortization modeling
Request Body:
{
  "plaidUserId": "user_123",
  "userInputs": { "age": 32, "riskTolerance": "medium" },
  "goal": { "targetAmount": 50000, "timelineMonths": 36, "goalType": "savings" },
  "simulationParams": { "nSimulations": 100000 }
}
Response includes enhanced metadata:
{
  "successProbability": 0.73,
  "percentiles": { /* ... */ },
  "assumptions": {
    "dataSource": "plaid",
    "accountsIncluded": {
      "depository": 2,
      "credit": 3,
      "loans": 1,
      "investments": 1
    },
    "derivedParams": {
      "annualReturnMean": 0.082,
      "annualReturnStd": 0.16,
      "incomeVolatility": 0.04,
      "expenseVolatility": 0.14,
      "creditCardsModeled": 3,
      "loansModeled": 1
    }
  }
}

POST /api/sensitivity

Runs sensitivity analysis. Request Body: Same as /api/simulate Response:
{
  "baseProbability": 0.73,
  "sensitivities": {
    "income_plus_10": { "delta": 0.12, "newProbability": 0.85, "impact": 0.12 },
    "income_minus_10": { "delta": -0.14, "newProbability": 0.59, "impact": 0.14 },
    "spending_minus_10": { "delta": 0.15, "newProbability": 0.88, "impact": 0.15 },
    "spending_plus_10": { "delta": -0.13, "newProbability": 0.60, "impact": 0.13 },
    "timeline_plus_6mo": { "delta": 0.18, "newProbability": 0.91, "impact": 0.18 }
  },
  "mostImpactful": "timeline_plus_6mo",
  "recommendations": [
    "Extending your timeline by 6 months gives more room for growth.",
    "Reducing spending by 10% could significantly improve your odds."
  ]
}

Plaid Routes

Location: src/routes/plaid.ts

POST /api/plaid/create-link-token

Generates Plaid Link token for frontend.

POST /api/plaid/exchange-public-token

Exchanges public token for access token after user links account.

GET /api/plaid/accounts

Fetches all linked accounts for a user.

AI Routes

Location: src/routes/ai.ts

POST /api/ai/parse-goal

Parses natural language goal using Gemini. Request:
{
  "goalText": "I want to save up for a down payment on a house, around $80,000, hopefully in the next 4 years"
}
Response:
{
  "targetAmount": 80000,
  "timelineMonths": 48,
  "goalType": "home_purchase",
  "confidence": 0.95
}

POST /api/ai/voice-input

Processes voice audio with ElevenLabs speech-to-text.

Services

SimulationService

Location: src/services/simulationService.ts Orchestrates Python Monte Carlo engine.
export class SimulationService {
  private simulationDir = path.resolve(process.cwd(), '../../simulation')
  private pythonPath = path.join(this.simulationDir, 'venv/bin/python3')

  async runSimulation(request: SimulationRequest): Promise<SimulationResults> {
    // Try Python simulation first
    try {
      return await this.runPythonSimulation(request)
    } catch (error) {
      console.warn('Python simulation failed, using JS fallback')
      return this.runJSSimulation(request)
    }
  }

  private async runPythonSimulation(request: SimulationRequest) {
    return new Promise((resolve, reject) => {
      const process = spawn(this.pythonPath, [
        path.join(this.simulationDir, 'main.py'),
        '--mode', 'simulate',
        '--input', JSON.stringify(request),
      ])

      let stdout = ''
      process.stdout.on('data', (data) => stdout += data.toString())
      process.on('close', (code) => {
        if (code === 0) {
          resolve(JSON.parse(stdout))
        } else {
          reject(new Error('Python simulation failed'))
        }
      })
    })
  }

  // JavaScript fallback for development without Python
  private runJSSimulation(request: SimulationRequest): SimulationResults {
    // Simplified Monte Carlo in TypeScript
    // ... implementation
  }
}
The service includes a JavaScript fallback for development environments without Python setup.

PlaidService

Location: src/services/plaidService.ts Manages Plaid API integration.
import { PlaidApi, Configuration, PlaidEnvironments } from 'plaid'

const plaidClient = new PlaidApi(
  new Configuration({
    basePath: PlaidEnvironments[process.env.PLAID_ENV || 'sandbox'],
    baseOptions: {
      headers: {
        'PLAID-CLIENT-ID': process.env.PLAID_CLIENT_ID,
        'PLAID-SECRET': process.env.PLAID_SECRET,
      },
    },
  })
)

export async function createLinkToken(userId: string) {
  const response = await plaidClient.linkTokenCreate({
    user: { client_user_id: userId },
    client_name: 'Drift',
    products: ['auth', 'transactions', 'investments'],
    country_codes: ['US'],
    language: 'en',
  })
  return response.data.link_token
}

export async function getAllAccountData(accessToken: string) {
  const [accounts, transactions, investments] = await Promise.all([
    plaidClient.accountsGet({ access_token: accessToken }),
    plaidClient.transactionsGet({
      access_token: accessToken,
      start_date: '2023-01-01',
      end_date: '2024-12-31',
    }),
    plaidClient.investmentsHoldingsGet({ access_token: accessToken }),
  ])

  return { accounts, transactions, investments }
}

AccountMappers

Location: src/services/accountMappers.ts Maps Plaid data to enhanced financial profile.
export interface EnhancedFinancialProfile {
  depository: DepositoryAccount[]
  credit: CreditAccount[]
  loans: LoanAccount[]
  investments: InvestmentAccount[]
  income: IncomeProfile | null
  spending: SpendingProfile | null
  totalLiquid: number
  totalCreditDebt: number
  totalLoanDebt: number
  totalInvestments: number
  netWorth: number
}

export function mapAllAccounts(data: PlaidData): EnhancedFinancialProfile {
  // Map depository accounts
  const depository = data.accounts.filter(a => a.type === 'depository')
    .map(a => ({
      id: a.account_id,
      type: 'depository',
      subtype: a.subtype,
      name: a.name,
      balance: a.balances.current,
      available: a.balances.available,
    }))

  // Map credit accounts with APR calculation
  const credit = data.accounts.filter(a => a.type === 'credit')
    .map(a => ({
      id: a.account_id,
      type: 'credit',
      name: a.name,
      balance: a.balances.current,
      limit: a.balances.limit,
      utilization: a.balances.current / a.balances.limit,
      apr: calculateAPR(a),  // From transaction interest charges
      minimumPayment: calculateMinPayment(a.balances.current),
    }))

  // ... more mapping logic

  return { depository, credit, loans, investments, income, spending, /* ... */ }
}

GeminiService

Location: src/services/geminiService.ts AI-powered goal parsing using Google Gemini.
import { GoogleGenerativeAI } from '@google/generative-ai'

const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY!)
const model = genAI.getGenerativeModel({ model: 'gemini-pro' })

export async function parseGoalText(goalText: string) {
  const prompt = `
Extract the financial goal details from this text:
"${goalText}"

Return JSON with:
- targetAmount (number in dollars)
- timelineMonths (number)
- goalType (one of: savings, home_purchase, education, retirement, debt_payoff, custom)
- confidence (0-1)
`

  const result = await model.generateContent(prompt)
  const response = result.response.text()
  return JSON.parse(response)
}

ClusterService

Location: src/services/clusterService.ts Submits large simulations to HPC clusters for parallel execution.
export async function submitClusterJob(request: SimulationRequest) {
  // Generate SLURM job script
  const script = generateSlurmScript(request)
  
  // Submit to cluster via SSH
  const jobId = await submitToCluster(script)
  
  return { jobId, status: 'queued' }
}

export async function getJobStatus(jobId: string) {
  // Query SLURM for job status
  return { jobId, status: 'running', progress: 0.45 }
}

Type System

Location: src/types/index.ts

Core Types

export interface FinancialProfile {
  liquidAssets: number
  creditDebt: number
  loanDebt: number
  monthlyLoanPayments: number
  monthlyIncome: number
  monthlyBills: number
  monthlySpending: number
  spendingByCategory: Record<string, number>
  spendingVolatility: number
}

export interface UserInputs {
  monthlyIncome: number
  age: number
  riskTolerance: 'low' | 'medium' | 'high'
}

export interface SimulationRequest {
  financialProfile: FinancialProfile
  userInputs: UserInputs
  goal: {
    targetAmount: number
    timelineMonths: number
    goalType: string
  }
  simulationParams?: {
    nSimulations?: number
  }
}

export interface SimulationResults {
  successProbability: number
  medianOutcome: number
  percentiles: {
    p10: number
    p25: number
    p50: number
    p75: number
    p90: number
  }
  mean: number
  std: number
  worstCase: number
  bestCase: number
  assumptions?: Assumptions
}

Error Handling

Global error handler:
app.use((err: Error, req: express.Request, res: express.Response, next) => {
  console.error(`[${req.method} ${req.path}] Error:`, err.message, err.stack)
  res.status(500).json({ error: 'Internal server error' })
})
Route-level error handling:
router.post('/simulate', async (req, res) => {
  try {
    const results = await simulationService.runSimulation(req.body)
    res.json(results)
  } catch (error) {
    console.error('Simulation error:', error)
    res.status(500).json({ 
      error: 'Failed to run simulation',
      details: error instanceof Error ? error.message : String(error)
    })
  }
})

Development

Running Locally

npm run dev --workspace=apps/api
# Runs on http://localhost:3001 with auto-reload

Environment Variables

Create .env in monorepo root:
# Plaid
PLAID_CLIENT_ID=your_client_id
PLAID_SECRET=your_secret
PLAID_ENV=sandbox

# AI Services
GEMINI_API_KEY=your_gemini_key
ELEVENLABS_API_KEY=your_elevenlabs_key
OPENAI_API_KEY=your_openai_key

# Nessie
NESSIE_API_KEY=your_nessie_key

# Server
PORT=3001

Testing

npm test --workspace=apps/api
Example test (src/services/llmService.test.ts):
import { parseGoalText } from './geminiService'

describe('GeminiService', () => {
  it('should parse goal text correctly', async () => {
    const result = await parseGoalText(
      'Save $50,000 for a down payment in 3 years'
    )
    expect(result.targetAmount).toBe(50000)
    expect(result.timelineMonths).toBe(36)
    expect(result.goalType).toBe('home_purchase')
  })
})

Next Steps

Simulation Engine

Dive into the Python Monte Carlo engine internals

Frontend

Explore the Next.js frontend architecture

Build docs developers (and LLMs) love