# HOA LedgerIQ — AI API Configuration Guide ## Overview The **Benefit Calculator widget** currently runs entirely client-side using conservative fixed-rate math. This guide explains how to upgrade it to call a real AI API (Claude or OpenAI) so the recommendation text is generated dynamically by a language model. --- ## Architecture ``` Browser (app.js) └─► POST /api/calculate (server.js) └─► Anthropic / OpenAI API └─► Returns AI-generated investment recommendation text └─► JSON response back to browser ``` --- ## Step 1 — Add your API key to `.env` Open `.env` in the project root and add one of the following: ```env # For Claude (Anthropic) — recommended ANTHROPIC_API_KEY=sk-ant-... # OR for OpenAI OPENAI_API_KEY=sk-... ``` --- ## Step 2 — Install the SDK ```bash # Claude (Anthropic) npm install @anthropic-ai/sdk # OR OpenAI npm install openai ``` --- ## Step 3 — Add the `/api/calculate` endpoint to `server.js` Add this block after the existing `/api/health` route: ### Using Claude (Anthropic) ```js const Anthropic = require('@anthropic-ai/sdk'); const anthropic = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY }); app.post('/api/calculate', async (req, res) => { const { homesites, propertyType, annualIncome, paymentFreq, reserveFunds, interest2025 } = req.body ?? {}; if (!homesites || !annualIncome) { return res.status(400).json({ error: 'homesites and annualIncome are required.' }); } const fmt = n => '$' + Math.round(n).toLocaleString(); const freqLabel = { monthly: 'monthly', quarterly: 'quarterly', annually: 'annual' }[paymentFreq] || 'monthly'; const typeLabel = { sfh: 'single-family home', townhomes: 'townhome', condos: 'condo', mixed: 'mixed-use' }[propertyType] || ''; const prompt = `You are a conservative HOA financial advisor. Given the following community data, provide a brief (3-4 sentence) plain-English investment income recommendation. Use only conservative, realistic estimates. Do not speculate beyond what the data supports. Community: ${homesites}-unit ${typeLabel} association Annual dues income: ${fmt(annualIncome)} (collected ${freqLabel}) Reserve fund balance: ${fmt(reserveFunds || 0)} Interest income earned in 2025: ${fmt(interest2025 || 0)} Provide a recommendation focused on: 1. How much of the reserve funds could conservatively be invested and in what vehicle (e.g. CD ladder, money market, T-bills) 2. How much operating cash could earn interest between collection and expense periods 3. A realistic estimated annual interest income potential 4. A single sentence comparing that to their 2025 actual if provided Keep the tone professional and factual. No bullet points — flowing paragraph only.`; try { const message = await anthropic.messages.create({ model: 'claude-opus-4-6', max_tokens: 300, messages: [{ role: 'user', content: prompt }], }); const text = message.content[0]?.text ?? ''; res.json({ recommendation: text }); } catch (err) { console.error('AI API error:', err.message); res.status(502).json({ error: 'AI service unavailable. Showing estimated result.' }); } }); ``` ### Using OpenAI (GPT-4o) ```js const OpenAI = require('openai'); const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY }); app.post('/api/calculate', async (req, res) => { const { homesites, propertyType, annualIncome, paymentFreq, reserveFunds, interest2025 } = req.body ?? {}; // ... (same prompt construction as above) ... try { const completion = await openai.chat.completions.create({ model: 'gpt-4o', max_tokens: 300, messages: [{ role: 'user', content: prompt }], }); const text = completion.choices[0]?.message?.content ?? ''; res.json({ recommendation: text }); } catch (err) { console.error('AI API error:', err.message); res.status(502).json({ error: 'AI service unavailable.' }); } }); ``` --- ## Step 4 — Update `app.js` to call the API endpoint In the `initCalculator` function, replace this line in the submitBtn handler: ```js document.getElementById('calcAiText').textContent = ai; // current: client-side text ``` With this: ```js // Call the AI endpoint; fall back to client-side text if unavailable try { const aiRes = await fetch('/api/calculate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ homesites, propertyType, annualIncome, paymentFreq, reserveFunds, interest2025 }), }); if (aiRes.ok) { const { recommendation } = await aiRes.json(); document.getElementById('calcAiText').textContent = recommendation; } else { document.getElementById('calcAiText').textContent = ai; // fallback } } catch (_) { document.getElementById('calcAiText').textContent = ai; // fallback } ``` > Note: The `submitBtn` handler must be declared `async` for the `await` above to work: > ```js > submitBtn?.addEventListener('click', async () => { ... }); > ``` --- ## Step 5 — Restart the server ```bash sudo systemctl restart hoaledgeriqweb # Verify the endpoint is live curl -X POST http://localhost:3000/api/calculate \ -H "Content-Type: application/json" \ -d '{"homesites":150,"propertyType":"sfh","annualIncome":300000,"paymentFreq":"monthly","reserveFunds":500000,"interest2025":4200}' ``` --- ## Prompt Tuning Tips The prompt in Step 3 is the core of the AI's behavior. You can adjust it to: | Goal | Change | |---|---| | More optimistic estimates | Change "conservative" to "moderate" in the prompt | | Shorter output | Reduce `max_tokens` to `150` | | Include specific investment products | Add "mention specific products like Vanguard Federal Money Market or 6-month T-bills" | | Add a disclaimer | Append "End with one sentence reminding them this is not financial advice." | --- ## Cost Estimate | Model | Approx. cost per calculator use | |---|---| | Claude Opus 4.6 | ~$0.002 | | Claude Sonnet 4.6 | ~$0.0004 | | GPT-4o | ~$0.002 | | GPT-4o-mini | ~$0.00005 | For a landing page with low traffic, even Claude Opus is negligible cost. For scale, `claude-sonnet-4-6` is the best balance of quality and price. --- ## Security Notes - **Never expose your API key in `app.js` or any client-side code.** All AI calls must go through `server.js`. - Rate-limit the `/api/calculate` endpoint to prevent abuse (e.g. with `express-rate-limit`): ```bash npm install express-rate-limit ``` ```js const rateLimit = require('express-rate-limit'); const calcLimiter = rateLimit({ windowMs: 60 * 1000, max: 10 }); // 10 req/min per IP app.use('/api/calculate', calcLimiter); ```