Minty AI
Minty AI
Component Files:
src/features/tools/components/MintyAI.tsx(Full page)src/features/tools/components/MintyAIDropdown.tsx(Header dropdown)
Edge Function: chat-completion (Supabase Edge Function v21) Route / navigation: Browser path `/mintyai` (PageView mintyai; sidebar/header entry, not a hub tool grid). See PageRouter.tsx + src/store/types.ts. Access Level: parent_org and fund_user user types only (not donor/volunteer) Last Updated: March 24, 2026 Status: ✅ Active (with Real-Time Data Access + Semantic Search + Navigation Links)
Overview
Minty AI is an AI-powered chat assistant integrated into Alignmint to help nonprofit accountants and administrators with questions about their data, math calculations, and accounting concepts. It provides a conversational interface with real-time database access to answer questions about donors, donations, volunteers, and more.
Key Features:
- Minty AI can query your nonprofit's actual data from Supabase to answer questions like "Who are my top donors?" or "How much did we raise this month?"
- Responses include nav:// hotlinks that navigate users directly to relevant reports and tools
- Query templates are matched inside Minty (semantic / embeddings). Open Minty from the header dropdown or sidebar; the header global search modal (Cmd/Ctrl+K) is navigation + people only and does not list Minty templates (see DEVELOPER-PLAYBOOK §31)
For questions about how to use the Alignmint application itself, users can visit Alignmint Support at getalignmint.org/ask (link in footer).
Recent Updates (March 24, 2026)
- Documentation — Clarified that the header global search modal (Cmd/Ctrl+K) does not surface Minty query templates; use Minty from the header or sidebar. Updated 01-HEADER.md, DEVELOPER-PLAYBOOK §31, and archive GLOBAL-SEARCH.md.
Previous Updates (March 20, 2026)
- Reconciled Transaction Queries Added - Minty now handles accounting prompts like:
- "show me all transactions that were marked reconciled after 3/4/26"
- "show reconciled transactions"
- "entries reconciled since last month"
- New query function:
getReconciledTransactions - New semantic template slug:
reconciled_transactions - Semantic False-Match Guardrail - Low-confidence semantic matches now return a clarification message for accounting-style prompts instead of executing a potentially wrong report
- Semantic execution threshold raised to
0.45 - Weak match zone
0.45-0.55now asks for clarification when intent is ambiguous - Dropdown Tier Context Parity -
MintyAIDropdown.tsxnow sendsorgTier,isParentOrg, andcustomPermissionstochat-completion, matching full-page Minty behavior - Balance Sheet RPC Param Fixes -
getReserveBalanceandgetOpeningBalancesnow callget_balance_sheet_datawithp_org_id(singular) to preserve entity scoping
Previous Updates (January 18, 2026)
- Response Feedback System - Users can now report incorrect AI responses
- Flag icon on each AI response opens feedback modal
- Captures prompt, response, conversation history, and user description
- Stored in
chat_feedbacktable for admin review - Rate limited to 10 feedback submissions per day
- New
submit-chat-feedbackedge function deployed - Entity Context in Responses - Query results now show which organization was queried
- Responses include entity name when filtered (e.g., "You have 0 recurring donors for Awakenings")
- Zero-result responses suggest selecting "All Nonprofits" to see data across all organizations
- Helps users understand why results may differ from expectations
- AlignmintGPT Link - Added "App help? Ask AlignmintGPT" link in footer
Previous Updates (January 15, 2026)
- Document/Dropbox Integration - MintyAI can now search the Drop Box for documents
- Ask "Pull most recent 990" to find Form 990 tax returns
- Ask "Where is our W-9?" to find W-9 forms
- Ask "Find the audit" to find audit documents
- Ask "Tax documents?", "Contracts?", "Receipts?" to browse by category
- If document not found, MintyAI provides upload instructions with nav link to Drop Box
- 8 New Query Functions -
getForm990Documents,getW9Documents,getAuditDocuments,getFinancialDocuments,getTaxDocuments,getContractDocuments,getReceiptDocuments,searchDropboxFiles - 8 New Query Templates - Added semantic search templates for document queries
Previous Updates (January 13, 2026)
- Prospect Search by Name - Ask "Is John Smith a prospect?" to search prospects by name
- Prospect List with Jump-Offs - Ask "Pull me my prospect list" to get clickable prospect list
- Mailing Address Query - Ask "Where do donors mail checks?" to get organization mailing address
- Clickable Jump-Off Links - Donors, prospects, and donations now include clickable links to their profiles
- FK Hint Fix - Added explicit foreign key hints (
!donor_id) to donation/subscription queries to prevent PostgREST 300 errors - All Embeddings Verified - Confirmed all 59 query templates have embeddings generated
Previous Updates (December 30, 2025)
- Full Edge Function Restored - Replaced lightweight version with comprehensive 2763-line implementation
- 59 Query Templates - All templates now active with semantic search matching
- Navigation Links Restored - Responses include clickable
nav://links to relevant tools/reports - Search integration (historical) - Query templates were previously surfaced from the header global search modal; as of March 2026 that modal no longer shows Minty rows. Templates are used from within Minty;
pendingMintyPromptremains available for future programmatic pre-fill. - Instructional Question Exclusion - How-to questions are now properly handled by the LLM, not matched as data queries
- QUERY_FUNCTION_REGISTRY - 40+ query functions mapped to database operations
Previous Updates (December 18, 2025)
- Semantic Search - Vector-based query matching using pgvector and OpenRouter embeddings
- Accounting Queries - Cash balance, bank balances, net income, spending/revenue by account, AR/AP
- Prospect Queries - Count, by status, recent prospects
- Event Queries - Revenue, ticket sales, registrations
- Marketing Queries - Text campaigns, email stats, campaign performance
- Goal Tracking - Donation goal progress from dashboard settings, YoY comparison
- Standardized on GPT-4o-mini - Removed model selector, single model for simplicity
- Unmatched Query Logging - Logs queries that don't match for future template creation
Features
Core Functionality
- Real-time data access - Queries Supabase to answer questions about your nonprofit
- Text-based chat - Simple conversational interface
- Semantic search - Understands natural language variations via vector embeddings
- Conversation history - Persisted per-user in Supabase, shared between full page and dropdown
- Clear all history - Delete all chat history with one click (appears when history exists)
- Rate limiting - Prevents abuse and controls costs (shared across both views)
- Mobile responsive - Full functionality on all screen sizes (full page only)
- Entity-aware - Respects selected nonprofit filter for data queries
- Header dropdown - Quick access from any page via robot icon (desktop only, matches NotificationPanel sizing)
- Model persistence - Selected model saved to
user_preferences.mintyai_model
Chat History UI
- Sidebar Minty AI: Shows up to 10 conversations visible, rest scrollable
- Header Minty AI: Shows up to 5 conversations visible, rest scrollable
- Clear All History: Red trash icon button at bottom of history dropdown (only visible when history exists)
- New Chat button: Plus icon in header bar for quick new conversation
AI Model
Minty AI uses GPT-4o-mini via OpenRouter for all responses. This provides the best balance of speed, cost, and quality for nonprofit data queries.
| Model | Provider | Cost (Input/Output per 1M tokens) |
|-------|----------|-----------------------------------|
| GPT-4o-mini | OpenAI (via OpenRouter) | $0.15 / $0.60 |
Rate Limits
- 50 messages per day per user
- 5 messages per minute (spam prevention)
- 2,000 token limit per response
- 4,000 token context window (recent conversation history)
Supported Data Queries
Minty AI can answer these questions by querying your actual database:
| Category | Example Questions |
|----------|-------------------|
| **Donors** | "Top donors?", "How many donors?", "Recurring donors?", "Lapsed donors?", "At-risk donors?", "Find donor John Smith" |
| **Prospects** | "How many prospects?", "Prospects by status?", "Recent prospects?", "Is John Smith a prospect?", "Pull me my prospect list" |
| **Donations** | "How much this month/year?", "Recent donations?", "Average donation?", "Failed payments?" |
| **Organization** | "Where do donors mail checks?", "Mailing address?", "EIN?", "Organization info?" |
| **Goals** | "Are we on pace to hit our goal?", "Compare to last year?" |
| **Subscriptions** | "Active subscriptions?", "MRR?", "Who cancelled?" |
| **Marketing** | "Campaign performance?", "Email stats?", "Text campaigns?" |
| **Events** | "Upcoming events?", "Ticket sales?", "Event revenue?" |
| **Accounting** | "Cash balance?", "Bank balances?", "Net income?", "Spending by account?", "Show reconciled transactions after 3/4/26" |
| **Finance** | "Accounts receivable?", "Accounts payable?", "Revenue by account?" |
| **Expenses** | "Total expenses?", "Expenses by category?" |
| **Volunteers** | "Volunteer count?", "Volunteer hours?" |
| **Contacts** | "How many contacts?", "SMS contacts?" |
| **Documents** | "Pull most recent 990", "Where is our W-9?", "Find the audit", "Tax documents?", "Contracts?", "Receipts?" |
| **Capabilities** | "What can you do?", "Help" |
Note: Data queries respect the currently selected entity filter. If viewing "Awakenings", queries return data for Awakenings only. If viewing "All Nonprofits", queries return aggregate data.
Entity ID Mapping
The frontend uses organization slugs (e.g., "infocus") but the database uses UUIDs. The getOrgId() function from src/lib/entityMapping.ts handles this conversion:
import { getOrgId } from '../lib/entityMapping';
// In API call:
entityId: getOrgId(selectedEntity) // Converts "infocus" → UUID or null for "all"UI Design
Desktop Layout
┌─────────────────────────────────────────────────────────────┐
│ Minty AI [Model ▼] │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 🤖 Hi! I'm Minty AI. I can help with math, │ │
│ │ accounting questions, and more. What's up? │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 👤 Can you help me calculate the depreciation │ │
│ │ for a $50,000 asset over 5 years? │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 🤖 Sure! Using straight-line depreciation: │ │
│ │ $50,000 ÷ 5 years = $10,000/year 📊 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
├─────────────────────────────────────────────────────────────┤
│ [Type your message... ] [Send] │
│ │
│ 45/50 messages remaining today │
└─────────────────────────────────────────────────────────────┘Mobile Layout
┌───────────────────────────┐
│ Minty AI [Model ▼]│
├───────────────────────────┤
│ │
│ ┌───────────────────────┐ │
│ │ 🤖 Hi! I'm Minty AI. │ │
│ │ What can I help with? │ │
│ └───────────────────────┘ │
│ │
│ ┌───────────────────────┐ │
│ │ 👤 Calculate 15% of │ │
│ │ $2,500 │ │
│ └───────────────────────┘ │
│ │
│ ┌───────────────────────┐ │
│ │ 🤖 $375! 🧮 │ │
│ └───────────────────────┘ │
│ │
├───────────────────────────┤
│ [Message...] [Send] │
│ 48/50 remaining │
└───────────────────────────┘Data Model
chat_conversations Table
CREATE TABLE chat_conversations (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
title TEXT DEFAULT 'New Chat',
created_at TIMESTAMP WITH TIME ZONE DEFAULT now(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT now()
);
-- Index for fetching user's conversations
CREATE INDEX idx_chat_conversations_user_id ON chat_conversations(user_id, updated_at DESC);
-- RLS Policies
ALTER TABLE chat_conversations ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Users can view own conversations" ON chat_conversations
FOR SELECT USING (auth.uid() = user_id);
CREATE POLICY "Users can insert own conversations" ON chat_conversations
FOR INSERT WITH CHECK (auth.uid() = user_id);
CREATE POLICY "Users can update own conversations" ON chat_conversations
FOR UPDATE USING (auth.uid() = user_id);
CREATE POLICY "Users can delete own conversations" ON chat_conversations
FOR DELETE USING (auth.uid() = user_id);chat_messages Table
CREATE TABLE chat_messages (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
conversation_id UUID REFERENCES chat_conversations(id) ON DELETE CASCADE,
role TEXT NOT NULL CHECK (role IN ('user', 'assistant', 'system')),
content TEXT NOT NULL,
model TEXT, -- 'gpt-4o-mini', 'claude-3-haiku', 'llama-3.1-8b'
tokens_used INTEGER DEFAULT 0,
created_at TIMESTAMP WITH TIME ZONE DEFAULT now()
);
-- Index for fetching user's conversation history
CREATE INDEX idx_chat_messages_user_id ON chat_messages(user_id, created_at DESC);
CREATE INDEX idx_chat_messages_conversation_id ON chat_messages(conversation_id, created_at ASC);
-- RLS Policy: Users can only see their own messages
ALTER TABLE chat_messages ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Users can view own messages" ON chat_messages
FOR SELECT USING (auth.uid() = user_id);
CREATE POLICY "Users can insert own messages" ON chat_messages
FOR INSERT WITH CHECK (auth.uid() = user_id);chat_rate_limits Table
CREATE TABLE chat_rate_limits (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
date DATE NOT NULL DEFAULT CURRENT_DATE,
message_count INTEGER DEFAULT 0,
last_message_at TIMESTAMP WITH TIME ZONE DEFAULT now(),
UNIQUE(user_id, date)
);
-- RLS Policy
ALTER TABLE chat_rate_limits ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Users can view own rate limits" ON chat_rate_limits
FOR SELECT USING (auth.uid() = user_id);
CREATE POLICY "Users can upsert own rate limits" ON chat_rate_limits
FOR ALL USING (auth.uid() = user_id);chat_feedback Table (Added Jan 18, 2026)
CREATE TABLE chat_feedback (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
conversation_id UUID REFERENCES chat_conversations(id) ON DELETE SET NULL,
message_id TEXT, -- The assistant message ID being reported
user_prompt TEXT NOT NULL, -- The user's original question
ai_response TEXT NOT NULL, -- The AI's response being reported
feedback_text TEXT NOT NULL, -- User's description of what was wrong
entity_id TEXT, -- Which entity was selected when query ran
chat_history JSONB, -- Full conversation context
metadata JSONB, -- Browser info, timestamps, etc.
status TEXT DEFAULT 'pending', -- 'pending', 'reviewed', 'resolved', 'dismissed'
reviewed_by UUID REFERENCES auth.users(id) ON DELETE SET NULL,
reviewed_at TIMESTAMPTZ,
resolution_notes TEXT,
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- RLS Policies
ALTER TABLE chat_feedback ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Users can insert own feedback" ON chat_feedback
FOR INSERT WITH CHECK (auth.uid() = user_id);
CREATE POLICY "Users can view own feedback" ON chat_feedback
FOR SELECT USING (auth.uid() = user_id);
CREATE POLICY "Parent org admins can view all feedback" ON chat_feedback
FOR SELECT USING (
EXISTS (
SELECT 1 FROM organization_users ou
WHERE ou.user_id = auth.uid() AND ou.role = 'parent_org'
)
);API Integration
Environment Variables
OPENROUTER_API_KEY=sk-or-v1-xxxxxAdd this to Supabase Edge Function Secrets (not Vercel) with the exact name OPENROUTER_API_KEY.
To set the secret: 1. Go to Supabase Dashboard → Edge Functions → Secrets 2. Add OPENROUTER_API_KEY with your OpenRouter API key value
Edge Function: chat-completion
Endpoint: POST /functions/v1/chat-completion
Request:
{
messages: Array<{
role: 'user' | 'assistant' | 'system';
content: string;
}>;
model: 'gpt-4o-mini' | 'claude-3-haiku' | 'llama-3.1-8b';
conversationId?: string; // UUID of the conversation session
}Response:
{
content: string;
model: string;
tokens_used: number;
remaining_messages: number;
}Error Responses:
| Status | Error | Description |
|--------|-------|-------------|
| 401 | Unauthorized | User not authenticated |
| 429 | Rate Limited | Daily or per-minute limit exceeded |
| 500 | Server Error | OpenRouter API error |
OpenRouter Model IDs
const MODEL_IDS = {
'gpt-4o-mini': 'openai/gpt-4o-mini',
'claude-3-haiku': 'anthropic/claude-3-haiku',
'llama-3.1-8b': 'meta-llama/llama-3.1-8b-instruct',
};System Prompt
You are Minty AI, a helpful AI assistant for nonprofit accountants and administrators using Alignmint.
Your personality:
- Friendly and approachable, but concise to save tokens
- Good with numbers and accounting concepts
- Use emojis sparingly (1-2 per response max)
- Keep responses brief - aim for 2-3 sentences when possible
You can help with:
- Math calculations (percentages, depreciation, allocations, etc.)
- General accounting concepts
- Nonprofit finance questions
- General knowledge questions
When users ask "what can you do?" or similar, tell them you can answer questions about their nonprofit data like:
- "Who are my top donors?"
- "How much did we raise this month?"
- "How many volunteers do we have?"
- "What's our average donation?"
- Plus math calculations and accounting concepts.
Important rules:
- Never share sensitive financial advice - recommend consulting a CPA for complex tax/legal matters
- Be honest if you don't know something
- Keep responses professional but warm! 🌱Data Access Architecture
Minty AI uses a hybrid approach combining regex pattern matching and semantic search:
Phase 1: Regex Pattern Matching (Fast)
// ~25 regex patterns for common queries
// Patterns support natural phrasings: "pull my...", "show me...", "give me..."
const DATA_PATTERNS = {
topDonors: /top\s*(\d+)?\s*donors?|biggest\s*donors?|(show|give|pull|get)\s*(me\s*)?(my\s*)?top\s*donors?/i,
incomeStatement: /income\s*statement|(show|give|pull|get)\s*(me\s*)?(my\s*|the\s*)?income\s*statement/i,
balanceSheet: /balance\s*sheet|(show|give|pull|get)\s*(me\s*)?(my\s*|the\s*)?balance\s*sheet/i,
givingTrends: /giving\s*trends?|compare\s*(to|with)?\s*(last\s*)?year|how\s*(do|did)\s*we\s*compare/i,
// ... 21 more patterns
};Phase 2: Semantic Search Fallback (Flexible)
If regex doesn't match, the system uses vector embeddings via pgvector:
// Generate embedding for user question
const embedding = await generateEmbedding(message); // OpenRouter text-embedding-3-small
// Find matching template using cosine similarity
const match = await supabase.rpc('match_query_template', {
query_embedding: embedding,
match_threshold: 0.45, // execution threshold
match_count: 1
});
// Guardrail for weak matches
if (match?.similarity >= 0.45 && match.similarity < 0.55) {
// return clarification for ambiguous accounting prompts
}Query Templates Database
The query_templates table contains 60 templates with:
slug- Unique identifier (e.g.,top_donors)canonical_question- Primary question textvariations- Array of alternative phrasingsembedding- 1536-dimension vector for semantic matchingquery_function- Function name in QUERY_FUNCTION_REGISTRY
Query Function Registry
const QUERY_FUNCTION_REGISTRY = {
getTopDonors, getDonorCount, getRecurringDonors,
getTotalDonations, getMonthlyDonations, getYearlyDonations,
getBalanceSheet, getIncomeStatement, getCashBalance,
// ... 40+ functions total
};Processing Flow
User Message
│
▼
┌─────────────────────────────────────┐
│ Phase 0: Instructional Exclusion │
│ (Skip "how do I..." questions) │
└─────────────────────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ Phase 1: Static Regex Matching │
│ (DATA_PATTERNS, fastest path) │
└─────────────────────────────────────┘
│
├── Match Found → Execute Query Function
│
▼
┌─────────────────────────────────────┐
│ Phase 1b: Dynamic Regex Matching │
│ (minty_dynamic_patterns from DB) │
└─────────────────────────────────────┘
│
├── Match Found → Execute Dynamic Function/Response
│
▼
┌─────────────────────────────────────┐
│ Phase 2: Semantic Search │
│ (Vector similarity via pgvector) │
└─────────────────────────────────────┘
│
├── Match Found (>0.45) → Execute Query Function
│
▼
┌─────────────────────────────────────┐
│ Phase 3: AI Fallback │
│ (GPT-4o-mini via OpenRouter) │
└─────────────────────────────────────┘Learning Pipeline Notes
- Unmatched prompts are written to
minty_unmatched_querieswhen no static/dynamic/semantic match is found. - Daily cron (
minty-daily-learning) triggersminty-learnto cluster unprocessed queries (default 168h window, config-drivenmin_query_count). minty-learnskips clusters already covered by static intents or existing dynamic regexes, and marks skipped clusters as processed to prevent reprocessing loops.- Internal admins can run manual training with optional force-learning window overrides (
sinceHours/includeOlder) for one-off backlog cleanup.
Navigation Links (nav:// Protocol)
Data query responses can include clickable navigation links:
// In query function response:
return `📊 **Balance Sheet**\n...\n\n[View Full Report](nav://reports/balance-sheet)`;The frontend renders these as buttons that navigate within the app.
Component Structure
Minty AI.tsx
interface Message {
id: string;
role: 'user' | 'assistant';
content: string;
createdAt: string;
}
interface Minty AIProps {
// Future: could accept initial context
}
// State
- messages: Message[]
- inputValue: string
- isLoading: boolean
- selectedModel: 'gpt-4o-mini' | 'claude-3-haiku' | 'llama-3.1-8b'
- remainingMessages: number
- error: string | nullKey Functions
loadConversationHistory()- Fetch recent messages from SupabasesendMessage()- Send message to Edge Function, save responsecheckRateLimit()- Verify user hasn't exceeded limitsclearConversation()- Start fresh (optional feature)
Security Considerations
API Key Protection
- OpenRouter API key stored in Vercel environment variables
- Never exposed to client - all calls go through Edge Function
- Edge Function validates user authentication before processing
Prompt Injection Prevention
- System prompt is server-side only
- User messages are sanitized before sending to API
- Response content is escaped before rendering
Rate Limiting
- Enforced server-side in Edge Function
- Database tracks daily usage per user
- Per-minute limiting prevents rapid-fire abuse
Data Privacy
- Chat history is per-user (RLS enforced)
- Users can only see their own conversations
- No cross-user data leakage possible
Future Enhancements
Potential Features (Not Planned)
- CSV export - Export query results to spreadsheet
- Suggested questions - Quick action buttons for common queries
- Context awareness - Pass current page context to Minty AI
Recently Implemented (v21)
- Navigation links - Clickable
nav://buttons to jump to Donor Hub, Reports, etc. (restored in v21) - Search integration — Query templates are used inside Minty; header global search is people + pages/tools only (see playbook §31)
Related Documentation
- EDGE-FUNCTIONS.md - Edge function setup
- DATABASE-SCHEMA.md - Database structure
- ../components/01-HEADER.md - Header global search vs Minty entry points
- ../../frontend/DEVELOPER-PLAYBOOK.md - §31 Global Search System (modal behavior)
- 02-APP-SIDEBAR.md - Sidebar navigation
Support Flow
User has question
│
▼
┌──────────────────┐
│ Is it about the │
│ AlignMint app? │
└────────┬─────────┘
│
┌────┴────┐
│ │
Yes No
│ │
▼ ▼
┌─────────┐ ┌─────────┐
│AlignMint│ │ Minty AI │
│ GPT │ │ │
│(external)│ │(in-app) │
└─────────┘ └─────────┘Synced from IFMmvp-Frontend documentation: pages/tools/07-MINTGPT.md
Ready to Get Started?
See how Alignmint can simplify your nonprofit's operations. Schedule a free demo with our team and we'll walk you through everything.
Questions? Email us at steven@getalignmint.org