# Project export: Finvisor

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: TreeHacks 2026
- Tagline: Easy and accurate AI financial aid appeal writer
- Devpost: https://devpost.com/software/finvisor-pxem15
- GitHub: https://github.com/blessitac/finvisor
- Video: https://player.vimeo.com/video/1165159067?byline=0&portrait=0&title=0#t=
- Team: 2 GitHub contributor(s) — openhands (2 commits), blessitac (1 commits)

## Devpost submission (written by the team)

### Inspiration

We've seen many students assume their dream school is unaffordable because they don’t know how to appeal for more aid. The process is hidden, intimidating, and usually reserved for families with expert guidance. We wanted to make that support accessible to everyone.

### What it does

Finvisor is an AI financial aid appeal writer. Students upload documents, describe their situation, and receive a personalized, professional letter grounded in how aid offices evaluate requests. It turns confusion into a clear next step.

### How we built it

We created a guided intake for financial context, captured key signals from conversations and documents, and used large language models to transform that information into persuasive, school-appropriate appeals fast.

### Challenges we ran into

Aid policies vary by institution, and tone matters. We had to balance flexibility, accuracy, privacy, and real strategic value.

### Accomplishments we're proud of

We made a complex process feel manageable in minutes. Families move from overwhelmed to empowered, with a letter they can confidently submit.

### What we learned

The real problem is information asymmetry. When students understand how decisions are made, they’re far more likely to advocate for themselves.

### What's next

We’re expanding beyond letters into a full financial advocacy platform with smarter document analysis, school-specific guidance, and deeper support for reducing total college cost.

## README (from the GitHub repository)

No README available.

## Detected evidence (automated analysis)

Indexed codebase: 33 recognized source files, 191 KB.
- Anthropic (technology) — detected in the code
- CSS (language) — detected in the code
- Next.js (technology) — detected in the code
- OpenAI (technology) — detected in the code
- React (technology) — detected in the code
- Tailwind CSS (technology) — detected in the code
- TypeScript (language) — detected in the code
- Vercel AI SDK (technology) — detected in the code
- JavaScript (language) — claimed on Devpost, not found in the code
- Node.js (technology) — claimed on Devpost, not found in the code

## Codebase structure (from repository index)

### Files (39 of 39)

```
finvisor/finvisor/.env.example
finvisor/finvisor/.gitignore
finvisor/finvisor/eslint.config.mjs
finvisor/finvisor/next.config.ts
finvisor/finvisor/package.json
finvisor/finvisor/postcss.config.mjs
finvisor/finvisor/README.md
finvisor/finvisor/src/app/api/analytics/route.ts
finvisor/finvisor/src/app/api/appeal/route.ts
finvisor/finvisor/src/app/api/chat/route.ts
finvisor/finvisor/src/app/api/documents/route.ts
finvisor/finvisor/src/app/api/payment/route.ts
finvisor/finvisor/src/app/api/research/route.ts
finvisor/finvisor/src/app/api/strategy/route.ts
finvisor/finvisor/src/app/api/submit/route.ts
finvisor/finvisor/src/app/api/zoom/route.ts
finvisor/finvisor/src/app/globals.css
finvisor/finvisor/src/app/layout.tsx
finvisor/finvisor/src/app/page.tsx
finvisor/finvisor/src/components/steps/index.ts
finvisor/finvisor/src/components/steps/StepAppeal.tsx
finvisor/finvisor/src/components/steps/StepDashboard.tsx
finvisor/finvisor/src/components/steps/StepFinnie.tsx
finvisor/finvisor/src/components/steps/StepResearch.tsx
finvisor/finvisor/src/components/steps/StepStrategy.tsx
finvisor/finvisor/src/components/steps/StepSubmit.tsx
finvisor/finvisor/src/components/steps/StepUpload.tsx
finvisor/finvisor/src/components/steps/StepZoom.tsx
finvisor/finvisor/src/components/ui.tsx
finvisor/finvisor/src/lib/anthropic.ts
finvisor/finvisor/src/lib/browserbase.ts
finvisor/finvisor/src/lib/decagon.ts
finvisor/finvisor/src/lib/fetchai.ts
finvisor/finvisor/src/lib/modal.ts
finvisor/finvisor/src/lib/openai.ts
finvisor/finvisor/src/lib/perplexity.ts
finvisor/finvisor/src/lib/zoom.ts
finvisor/finvisor/src/types/index.ts
finvisor/finvisor/tsconfig.json
```

### Dependencies

- finvisor/finvisor/package.json: @anthropic-ai/sdk@^0.74.0, @tailwindcss/postcss@^4, @types/node@^20, @types/react@^19, @types/react-dom@^19, ai@^6.0.86, axios@^1.13.5, eslint@^9, eslint-config-next@16.1.6, next@16.1.6, openai@^6.22.0, react@19.2.3, react-dom@19.2.3, tailwindcss@^4, typescript@^5, uuid@^13.0.0, zod@^4.3.6

### Recent commits (newest first)

- chore: revert zoom integration to demo mode for hackathon
- chore: add full finvisor app (remove embedded repo)
- feat: Zoom MVP conversion + S2S OAuth fixes

## Key source files (fetched from GitHub, selected and truncated for size)

### finvisor/finvisor/package.json

```
{
  "name": "finvisor",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "eslint"
  },
  "dependencies": {
    "@anthropic-ai/sdk": "^0.74.0",
    "ai": "^6.0.86",
    "axios": "^1.13.5",
    "next": "16.1.6",
    "openai": "^6.22.0",
    "react": "19.2.3",
    "react-dom": "19.2.3",
    "uuid": "^13.0.0",
    "zod": "^4.3.6"
  },
  "devDependencies": {
    "@tailwindcss/postcss": "^4",
    "@types/node": "^20",
    "@types/react": "^19",
    "@types/react-dom": "^19",
    "eslint": "^9",
    "eslint-config-next": "16.1.6",
    "tailwindcss": "^4",
    "typescript": "^5"
  }
}

```

### finvisor/finvisor/src/app/layout.tsx

```typescript
import type { Metadata } from "next";
import "./globals.css";

export const metadata: Metadata = {
  title: "Finvisor — AI Financial Aid Advisor",
  description: "AI-powered financial aid appeal platform. Get personalized advice, document analysis, and automated submission support.",
  keywords: ["financial aid", "college", "appeal", "AI advisor", "scholarship"],
  authors: [{ name: "Finvisor Team" }],
  openGraph: {
    title: "Finvisor — AI Financial Aid Advisor",
    description: "Build the strongest possible financial aid appeal with AI assistance",
    type: "website",
  },
};

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <body className="antialiased">
        {children}
      </body>
    </html>
  );
}

```

### finvisor/finvisor/src/app/page.tsx

```typescript
'use client';

// ═══════════════════════════════════════════════════════
// FINVISOR — Full 11-Step Hackathon Demo
// AI-powered financial aid appeal platform
// ═══════════════════════════════════════════════════════

import { useState } from 'react';
import { Background, Header, StepNav } from '@/components/ui';
import {
  StepFinnie,
  StepUpload,
  StepStrategy,
  StepResearch,
  StepAppeal,
  StepSubmit,
  StepZoom,
  StepDashboard,
} from '@/components/steps';

const STEP_LABELS = [
  'Finnie Chat',
  'Upload Docs',
  'Gap Strategy',
  'Research',
  'Generate Appeal',
  'Auto-Submit',
  'Live Advisor',
  'Dashboard',
];

export default function Finvisor() {
  const [step, setStep] = useState(0);

  const next = () => setStep((s) => Math.min(s + 1, STEP_LABELS.length - 1));

  return (
    <div style={{ minHeight: '100vh', position: 'relative' }}>
      {/* Google Fonts */}
      <link
        href="https://fonts.googleapis.com/css2?family=Outfit:wght@400;500;600;700;800&family=Fraunces:opsz,wght@9..144,700;9..144,800&display=swap"
        rel="stylesheet"
      />

      <Background />
      <Header />
      <StepNav current={step} labels={STEP_LABELS} />

      <div
        style={{
          padding: '8px 20px 60px',
          position: 'relative',
          zIndex: 2,
          display: 'flex',
          justifyContent: 'center',
        }}
      >
        {step === 0 && <StepFinnie onNext={next} />}
        {step === 1 && <StepUpload onNext={next} />}
        {step === 2 && <StepStrategy onNext={next} />}
        {step === 3 && <StepResearch onNext={next} />}
        {step === 4 && <StepAppeal onNext={next} />}
        {step === 5 && <StepSubmit onNext={next} />}
        {step === 6 && <StepZoom onNext={next} />}
        {step === 7 && <StepDashboard />}
      </div>
    </div>
  );
}

```

### finvisor/finvisor/src/types/index.ts

```typescript
// ═══════════════════════════════════════════════════════
// FINVISOR - Type Definitions
// ═══════════════════════════════════════════════════════

export interface Message {
  id: string;
  role: 'user' | 'assistant' | 'system';
  content: string;
  timestamp: Date;
  metadata?: {
    provider?: 'openai' | 'anthropic' | 'decagon';
    tokens?: number;
    latency?: number;
  };
}

export interface Document {
  id: string;
  type: 'w2' | 'fafsa' | 'aid_letter' | 'tax_return' | 'other';
  name: string;
  status: 'uploading' | 'parsing' | 'parsed' | 'error';
  parsedData?: ParsedDocumentData;
  rawContent?: string;
  createdAt: Date;
}

export interface ParsedDocumentData {
  type: string;
  fields: DocumentField[];
  confidence: number;
  rawExtraction?: Record<string, unknown>;
}

export interface DocumentField {
  key: string;
  value: string;
  flag?: boolean;
  confidence?: number;
}

export interface StudentProfile {
  id: string;
  school: string;
  currentAid: number;
  totalCost: number;
  gap: number;
  gpa?: number;
  circumstances: Circumstance[];
  documents: Document[];
}

export interface Circumstance {
  type: 'job_loss' | 'medical' | 'divorce' | 'death' | 'housing' | 'other';
  description: string;
  financialImpact?: number;
  date?: string;
}

export interface StrategyStep {
  id: string;
  label: string;
  result?: string;
  status: 'pending' | 'thinking' | 'done' | 'skipped';
  color?: string;
}

export interface ResearchResult {
  id: string;
  query: string;
  result: string;
  source: string;
  status: 'searching' | 'found' | 'error';
  citations?: Citation[];
}

export interface Citation {
  title: string;
  url: string;
  snippet: string;
}

export interface AppealLetter {
  id: string;
  content: string;
  status: 'generating' | 'complete' | 'error';
  metadata: {
    wordCount: number;
    citationsCount: number;
    tone: string;
    generatedBy: 'anthropic' | 'openai';
  };
}

export interface SubmissionStep {
  id: string;
  icon: string;
  text: string;
  status: 'pending' | 'running' | 'done' | 'error';
}

export interface ZoomSession {
  id: string;
  meetingId?: string;
  status: 'connecting' | 'live' | 'ended';
  transcript: TranscriptEntry[];
  insights: string[];
  startTime?: Date;
  endTime?: Date;
}

export interface TranscriptEntry {
  id: string;
  speaker: 'Advisor' | 'Student';
  text: string;
  timestamp: Date;
}

export interface PaymentPlan {
  id: string;
  name: string;
  price: number;
  features: string[];
  popular?: boolean;
}

export interface AgentTransaction {
  id: string;
  type: 'service_request' | 'payment' | 'completion';
  amount?: number;
  status: 'pending' | 'completed' | 'failed';
  agentAddress?: string;
  timestamp: Date;
}

export interface AnalyticsData {
  appealsFiled: number;
  successRate: number;
  avgAidIncrease: number;
  revenue: number;
  activeUsers: number;
}

export interface APIResponse<T> {
  success: boolean;
  data?: T;
  error?: string;
  metadata?: {
    provider: string;
    latency: number;
    tokensUsed?: number;
  };
}

```

### finvisor/finvisor/src/components/steps/index.ts

```typescript
export { default as StepFinnie } from './StepFinnie';
export { default as StepUpload } from './StepUpload';
export { default as StepStrategy } from './StepStrategy';
export { default as StepResearch } from './StepResearch';
export { default as StepAppeal } from './StepAppeal';
export { default as StepSubmit } from './StepSubmit';
export { default as StepZoom } from './StepZoom';
export { default as StepDashboard } from './StepDashboard';

```

### finvisor/finvisor/src/app/api/zoom/route.ts

```typescript
// ═══════════════════════════════════════════════════════
// FINVISOR - Zoom API Route (DEMO MODE)
// No real Zoom calls. Safe for hackathon demo.
// ═══════════════════════════════════════════════════════

import { NextRequest, NextResponse } from "next/server";

/**
 * DEMO MODE
 * This route simulates Zoom behavior without calling the real API.
 * No environment variables required.
 */

// Create a new "meeting"
export async function POST(request: NextRequest) {
  console.log("[API/Zoom] DEMO POST request received");

  const body = await request.json().catch(() => ({}));

  return NextResponse.json({
    success: true,
    data: {
      meetingId: "demo-12345",
      joinUrl: "https://zoom.us/j/DEMO_MEETING",
      startUrl: "https://zoom.us/s/DEMO_START",
      password: "123456",
      scheduledTime: body?.scheduledTime || "instant",
      topic: body?.topic || "Demo Advisory Session",
      duration: body?.duration || 30,
      instructions: [
        "Demo mode: no real Zoom meeting was created.",
        "Use this mock URL for presentation purposes.",
      ],
    },
  });
}

// Get meeting details
export async function GET() {
  console.log("[API/Zoom] DEMO GET request received");

  return NextResponse.json({
    success: true,
    data: {
      id: "demo-12345",
      topic: "Demo Advisory Session",
      status: "waiting",
      join_url: "https://zoom.us/j/DEMO_MEETING",
      start_url: "https://zoom.us/s/DEMO_START",
      password: "123456",
    },
  });
}

// Real-time transcript analysis (still works with Anthropic if you want)
export async function PUT(request: NextRequest) {
  console.log("[API/Zoom] DEMO PUT request received");

  const body = await request.json().catch(() => ({}));

  return NextResponse.json({
    success: true,
    data: {
      insights: [
        "Demo insight: recommend follow-up email within 7 days.",
        "Demo insight: gather additional financial documentation.",
      ],
      transcriptLength: body?.transcript?.length || 0,
    },
  });
}

// Webhook simulation
export async function PATCH() {
  console.log("[API/Zoom] DEMO PATCH request received");

  return NextResponse.json({
    success: true,
    received: "demo.webhook",
  });
}

```

### finvisor/finvisor/src/app/api/chat/route.ts

```typescript
// ═══════════════════════════════════════════════════════
// FINVISOR - Chat API Route
// Integrations: OpenAI (intake), Anthropic (reasoning), Decagon (UX)
// ═══════════════════════════════════════════════════════

import { NextRequest, NextResponse } from 'next/server';
import { generateChatResponse, type OpenAIMessage } from '@/lib/openai';
import { startConversation, sendMessage } from '@/lib/decagon';

export const runtime = 'edge';

interface ChatRequest {
  messages: Array<{ role: 'user' | 'assistant'; content: string }>;
  userId?: string;
  conversationId?: string;
  useDecagon?: boolean;
}

export async function POST(request: NextRequest) {
  try {
    const body: ChatRequest = await request.json();
    const { messages, userId, conversationId, useDecagon } = body;

    if (!messages || messages.length === 0) {
      return NextResponse.json(
        { error: 'Messages are required' },
        { status: 400 }
      );
    }

    // Check if we should use Decagon for conversational UX
    if (useDecagon && process.env.DECAGON_API_KEY) {
      let convId = conversationId;
      
      // Start new conversation if needed
      if (!convId && userId) {
        const conversation = await startConversation(userId);
        convId = conversation.id;
      }

      if (convId) {
        const lastMessage = messages[messages.length - 1];
        const decagonResponse = await sendMessage(convId, lastMessage.content);

        return NextResponse.json({
          success: true,
          data: {
            content: decagonResponse.response.content,
            conversationId: convId,
            intent: decagonResponse.intent,
            suggestedActions: decagonResponse.suggestedActions,
            provider: 'decagon',
          },
        });
      }
    }

    // Default to OpenAI for the conversational intake
    const openAIMessages: OpenAIMessage[] = messages.map((m) => ({
      role: m.role,
      content: m.content,
    }));

    const response = await generateChatResponse(openAIMessages);

    return NextResponse.json({
      success: true,
      data: {
        content: response.content,
        usage: response.usage,
        provider: 'openai',
      },
    });
  } catch (error) {
    console.error('Chat API error:', error);
    return NextResponse.json(
      {
        success: false,
        error: error instanceof Error ? error.message : 'Internal server error',
      },
      { status: 500 }
    );
  }
}

// Stream response for better UX
export async function GET(request: NextRequest) {
  const { searchParams } = new URL(request.url);
  const conversationId = searchParams.get('conversationId');

  if (!conversationId) {
    return NextResponse.json(
      { error: 'Conversation ID required' },
      { status: 400 }
    );
  }

  // Return conversation history
  // This would fetch from Decagon or a database
  return NextResponse.json({
    success: true,
    data: {
      conversationId,
      messages: [],
    },
  });
}

```

### finvisor/finvisor/src/app/api/analytics/route.ts

```typescript
// ═══════════════════════════════════════════════════════
// FINVISOR - Analytics API Route
// Aggregated data from all integrations
// ═══════════════════════════════════════════════════════

import { NextRequest, NextResponse } from 'next/server';
import { getAgentAnalytics } from '@/lib/fetchai';
import { getModalStatus } from '@/lib/modal';
import { getConversationAnalytics } from '@/lib/decagon';

export async function GET(request: NextRequest) {
  try {
    const { searchParams } = new URL(request.url);
    const timeRange = {
      start: searchParams.get('start') || new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString(),
      end: searchParams.get('end') || new Date().toISOString(),
    };

    // Gather analytics from all sources
    const [agentAnalytics, modalStatus, conversationAnalytics] = await Promise.allSettled([
      process.env.FETCHAI_API_KEY ? getAgentAnalytics() : Promise.resolve(null),
      process.env.MODAL_TOKEN_ID ? getModalStatus() : Promise.resolve(null),
      process.env.DECAGON_API_KEY ? getConversationAnalytics(timeRange) : Promise.resolve(null),
    ]);

    // Extract successful results
    const agent = agentAnalytics.status === 'fulfilled' ? agentAnalytics.value : null;
    const modal = modalStatus.status === 'fulfilled' ? modalStatus.value : null;
    const conversations = conversationAnalytics.status === 'fulfilled' ? conversationAnalytics.value : null;

    // Aggregate metrics
    const analytics = {
      overview: {
        totalAppeals: agent?.completedServices || 0,
        successRate: 68, // Would come from tracking actual outcomes
        avgAidIncrease: 8420, // Would be calculated from actual data
        revenue: agent?.totalRevenue || 0,
        activeUsers: conversations?.totalConversations || 0,
      },
      conversations: conversations
        ? {
            total: conversations.totalConversations,
            resolved: conversations.resolvedCount,
            escalated: conversations.escalatedCount,
            avgResolutionTime: conversations.averageResolutionTime,
            satisfaction: conversations.satisfactionScore,
            topIntents: conversations.topIntents,
          }
        : null,
      infrastructure: modal
        ? {
            healthy: modal.healthy,
            activeFunctions: modal.active_functions,
            gpuUtilization: modal.gpu_utilization,
            queueDepth: modal.queue_depth,
          }
        : null,
      payments: agent
        ? {
            totalRevenue: agent.totalRevenue,
            completedServices: agent.completedServices,
            activeRequests: agent.activeRequests,
            averageRating: agent.averageRating,
            recentTransactions: agent.recentTransactions,
          }
        : null,
      sponsors: {
        openai: { status: 'active', usage: 'Chat, Document Parsing' },
        anthropic: { status: 'active', usage: 'Strategy, Appeal Generation' },
        perplexity: { status: 'active', usage: 'Research' },
        browserbase: { status: 'active', usage: 'Auto-Submit' },
        zoom: { status: 'active', usage: 'Advisor Meetings' },
        modal: { status: modal?.healthy ? 'active' : 'inactive', usage: 'Inference' },
        fetchai: { status: 'active', usage: 'Payments' },
        decagon: { status: 'active', usage: 'Conversational UX' },
        vercel: { status: 'active', usage: 'Deployment' },
      },
    };

    return NextResponse.json({
      success: true,
      data: analytics,
      metadata: {
        generatedAt: new Date().toISOString(),
        timeRange,
      },
    });
  } catch (error) {
    console.error('Analytics error:', error);
    return NextResponse.json(
      {
        success: false,
        error: error instanceof Error ? error.message : 'Analytics fetch failed',
      },
      { status: 500 }
    );
  }
}

// Health check endpoint
export async function HEAD() {
  return new Response(null, {
    status: 200,
    headers: {
      'X-Status': 'healthy',
      'X-Version': '1.0.0',
    },
  });
}

```

### finvisor/finvisor/src/app/api/payment/route.ts

```typescript
// ═══════════════════════════════════════════════════════
// FINVISOR - Payment API Route
// Integrations: Fetch.ai (monetized agents)
// ═══════════════════════════════════════════════════════

import { NextRequest, NextResponse } from 'next/server';
import {
  requestPayment,
  confirmPayment,
  createServiceOffer,
  getAgentAnalytics,
  FINVISOR_SERVICES,
} from '@/lib/fetchai';

interface PaymentRequest {
  userId: string;
  service: 'basic_appeal' | 'pro_appeal' | 'premium_appeal' | 'advisor_session';
  amount?: number;
}

// Request payment for a service
export async function POST(request: NextRequest) {
  try {
    const body: PaymentRequest = await request.json();
    const { userId, service, amount } = body;

    if (!userId || !service) {
      return NextResponse.json(
        { error: 'User ID and service required' },
        { status: 400 }
      );
    }

    // Validate service type
    const validServices = ['basic_appeal', 'pro_appeal', 'premium_appeal', 'advisor_session'];
    if (!validServices.includes(service)) {
      return NextResponse.json(
        { error: 'Invalid service type' },
        { status: 400 }
      );
    }

    // Request payment through Fetch.ai agent
    const paymentRequest = await requestPayment(
      userId,
      service as PaymentRequest['service'],
      amount || 0
    );

    // Get service details for response
    const serviceKey = service.replace('_appeal', '') as keyof typeof FINVISOR_SERVICES;
    const serviceDetails = FINVISOR_SERVICES[serviceKey] || FINVISOR_SERVICES.basic;

    return NextResponse.json({
      success: true,
      data: {
        paymentId: paymentRequest.id,
        amount: paymentRequest.amount,
        currency: paymentRequest.currency,
        status: paymentRequest.status,
        service: {
          name: serviceDetails.name,
          description: serviceDetails.description,
          deliverables: serviceDetails.deliverables,
        },
        checkoutUrl: `https://agentverse.ai/pay/${paymentRequest.id}`,
      },
    });
  } catch (error) {
    console.error('Payment request error:', error);
    return NextResponse.json(
      {
        success: false,
        error: error instanceof Error ? error.message : 'Payment request failed',
      },
      { status: 500 }
    );
  }
}

// Confirm payment status
export async function GET(request: NextRequest) {
  try {
    const { searchParams } = new URL(request.url);
    const paymentId = searchParams.get('paymentId');
    const action = searchParams.get('action');

    // Get pricing/services info
    if (action === 'pricing') {
      return NextResponse.json({
        success: true,
        data: {
          services: Object.entries(FINVISOR_SERVICES).map(([key, value]) => ({
            id: `${key}_appeal`,
            ...value,
          })),
        },
      });
    }

    // Get agent analytics
    if (action === 'analytics') {
      const analytics = await getAgentAnalytics();
      return NextResponse.json({
        success: true,
        data: analytics,
      });
    }

    // Confirm specific payment
    if (!paymentId) {
      return NextResponse.json(
        { error: 'Payment ID required' },
        { status: 400 }
      );
    }

    const confirmation = await confirmPayment(paymentId);

    return NextResponse.json({
      success: true,
      data: {
        confirmed: confirmation.confirmed,
        transactionHash: confirmation.transaction_hash,
        amount: confirmation.amount,
        status: confirmation.confirmed ? 'completed' : 'pending',
      },
    });
  } catch (error) {
    console.error('Payment confirmation error:', error);
    return NextResponse.json(
      { success: false, error: 'Payment confirmation failed' },
      { status: 500 }
    );
  }
}

// Create service offer on marketplace
export async function PUT(request: NextRequest) {
  try {
    const body: {
      name: string;
      description: string;
      price: number;
      duration?: number;
      deliverables: string[];
    } = await request.json();

    const offer = await createServiceOffer(body);

    return NextResponse.json({
      success: true,
      data: {
        offerId: offer.offerId,
        listingUrl: offer.listingUrl,
        status: 'active',
      },
    });
  } catch (error) {
    console.error('Service offer error:', error);
    return NextResponse.json(
      { success: false, error: 'Offer creation failed' },
      { status: 500 }
    );
  }
}

```

### finvisor/finvisor/src/app/api/research/route.ts

```typescript
// ═══════════════════════════════════════════════════════
// FINVISOR - Research API Route
// Integrations: Perplexity (research), Modal (batch)
// ═══════════════════════════════════════════════════════

import { NextRequest, NextResponse } from 'next/server';
import {
  researchQuery,
  batchResearch,
  getSchoolComparison,
  researchAidPolicy,
  factCheck,
} from '@/lib/perplexity';

interface ResearchRequest {
  queries?: string[];
  school?: string;
  type?: 'general' | 'policy' | 'comparison' | 'fact_check';
  topic?: 'appeal_process' | 'special_circumstances' | 'deadlines' | 'requirements';
  comparisons?: string[];
  claim?: string;
}

export async function POST(request: NextRequest) {
  try {
    const body: ResearchRequest = await request.json();
    const { queries, school, type = 'general', topic, comparisons, claim } = body;

    let results;

    switch (type) {
      case 'policy':
        // Research specific school policies
        if (!school || !topic) {
          return NextResponse.json(
            { error: 'School and topic required for policy research' },
            { status: 400 }
          );
        }
        const policyResult = await researchAidPolicy(school, topic);
        results = {
          type: 'policy',
          school,
          topic,
          ...policyResult,
        };
        break;

      case 'comparison':
        // Get peer school comparison data
        if (!school) {
          return NextResponse.json(
            { error: 'School required for comparison' },
            { status: 400 }
          );
        }
        const comparisonResult = await getSchoolComparison(
          school,
          comparisons || ['Harvard', 'Yale', 'Princeton']
        );
        results = {
          type: 'comparison',
          targetSchool: school,
          ...comparisonResult,
        };
        break;

      case 'fact_check':
        // Verify a claim with citations
        if (!claim) {
          return NextResponse.json(
            { error: 'Claim required for fact checking' },
            { status: 400 }
          );
        }
        const factCheckResult = await factCheck(claim);
        results = {
          type: 'fact_check',
          claim,
          ...factCheckResult,
        };
        break;

      case 'general':
      default:
        // Batch research queries
        if (!queries || queries.length === 0) {
          // Generate default queries based on school
          const defaultQueries = school
            ? [
                `${school} average financial aid package 2025`,
                `Financial aid appeal success rate top universities`,
                `${school} financial aid appeal policy`,
                `COBRA insurance average cost 2025`,
                `Peer institution financial aid comparison Ivy+ schools`,
              ]
            : [];

          if (defaultQueries.length === 0) {
            return NextResponse.json(
              { error: 'Queries or school required' },
              { status: 400 }
            );
          }

          results = await batchResearch(defaultQueries);
        } else {
          results = await batchResearch(queries);
        }
        break;
    }

    return NextResponse.json({
      success: true,
      data: results,
      metadata: {
        provider: 'perplexity',
        model: 'sonar-pro',
      },
    });
  } catch (error) {
    console.error('Research API error:', error);
    return NextResponse.json(
      {
        success: false,
        error: error instanceof Error ? error.message : 'Research failed',
      },
      { status: 500 }
    );
  }
}

// Single query endpoint for real-time research
export async function GET(request: NextRequest) {
  try {
    const { searchParams } = new URL(request.url);
    const query = searchParams.get('query');
    const focus = searchParams.get('focus') as 'academic' | 'news' | 'general' || 'academic';

    if (!query) {
      return NextResponse.json(
        { error: 'Query parameter required' },
        { status: 400 }
      );
    }

    const result = await researchQuery(query, focus);

    return NextResponse.json({
      success: true,
      data: {
        query,
        ...result,
      },
      metadata: {
        provider: 'perplexity',
        focus,
      },
    });
  } catch (error) {
    console.error('Research GET error:', error);
    return NextResponse.json(
      { success: false, error: 'Research query failed' },
      { status: 500 }
    );
  }
}

```

[23 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]