# Project export: LandingMate AI

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: UC Berkeley AI Hackathon 2026
- Tagline: ASI:One + web app that turns one student message or I-20 into visa, housing, banking, timeline, and paid campus intelligence actions.
- Devpost: https://devpost.com/software/landingmate-ai
- GitHub: https://github.com/96528025/landingmate
- Video: https://www.youtube.com/embed/jPyU5gNHdhM?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 0 GitHub contributor(s) — 

## Devpost submission (written by the team)

### Inspiration

International students often arrive in the US with fragmented guidance: school check-in pages, visa rules, housing scams, banking setup, Reddit advice, and deadline confusion all live in different places. A missed check-in, fake sublease, or misunderstood document step can create real financial or immigration risk. We built LandingMate AI to turn one student message into a verified arrival action pack. Instead of giving generic advice, it coordinates specialized agents that produce official handoffs, ready-to-send messages, scam-screening artifacts, first-month budget planning, and optional paid local intelligence.

### What it does

LandingMate AI is an international student arrival control room with two demo modes: an ASI:One-compatible agent for the Fetch.ai track, and a React web app for the general track where judges can upload a sample I-20 PDF and inspect the generated arrival plan. A student can either message the public orchestrator in ASI:One or upload an I-20 in the web app. LandingMate extracts the student profile, resolves school resources, coordinates specialist agents, and returns a structured action pack with official handoffs, draft messages, scam checks, deadlines, budget planning, and local campus intelligence. For the general track, the React/Vite web app lets judges upload a sample I-20 PDF, watch the multi-agent workflow run, see the locked CampusVibeAgent state, and inspect generated artifacts such as DSO drafts, landlord verification messages, scam-screening checklists, official handoff links, budget plans, timeline items, and risk summaries. For the Fetch.ai track, the ASI:One orchestrator demonstrates the payment- gated agent flow: CampusVibeAgent stays locked until the user requests an unlock through the Agent Payment Protocol. The system includes: VisaCheckinAgent: creates official school check-in handoffs, I-94 reminders, and DSO email drafts. HousingSafetyAgent: generates housing scam-screening checklists, landlord verification messages, and deposit safety records. BankingPrepAgent: prepares no-SSN banking document packets and first-month budget plans. CampusVibeAgent: a premium agent that provides Reddit/community-based local housing and commute signals after payment unlock. RiskAgent: aggregates top risks across all agents. TimelineAgent: computes arrival deadlines from the student's program start date. FinalAdvisorAgent: formats everything into a plain-English action pack. The core loop is: Student sends one ASI:One message or uploads an I-20 PDF -> LandingMate extracts structured arrival context -> Specialist agents generate verified artifacts -> Risk and timeline agents consolidate priorities -> Student receives an actionable arrival control room -> Optional payment unlocks premium campus intelligence in ASI:One

### How we built it

LandingMate AI is built as a Python/FastAPI backend with Fetch.ai uAgents support, an Agentverse-facing orchestrator, and a React/Vite web app. The public Fetch.ai entrypoint is LandingMateOrchestratorAgent, which uses the Agent Chat Protocol so students can interact with it through ASI:One. The orchestrator parses student messages, asks follow-up questions when required fields are missing, resolves school information from trusted local source data, and coordinates internal specialist agents. For the general track, I restored and integrated a web app that gives judges a visible product surface. The web app supports sample I-20 PDF upload, agent activity logs, locked premium-agent state, extracted student profile display, top risk summaries, computed timeline items, trusted source links, and generated artifacts such as DSO drafts, landlord verification messages, scam- screening checklists, and first-month budget planning. The agent system supports two runtime modes: Function mode for stable hackathon demos. uAgent runtime mode for local Fetch.ai-style agent-to-agent messaging through a CoordinatorAgent and domain agents. I also implemented a payment-gated premium agent flow inspired by Fetch.ai's Agent Payment Protocol. Free agents always run, while CampusVibeAgent remains locked until the user requests an unlock. In the ASI:One flow, the orchestrator can create a payment request and verify payment before returning the premium campus intelligence report. The stack includes Python, FastAPI, Pydantic, Fetch.ai uAgents, Agent Chat Protocol, Agent Payment Protocol, Stripe test/demo flow, React, Vite, Tailwind CSS, TypeScript, Reddit public search, Claude optional refinement with deterministic fallback, and local trusted school/source data. A key design choice was privacy. LandingMate does not require accounts, login, Supabase, or long-term database storage for the main workflow. Student profile state is held in memory only for the active session, and uploaded demo PDFs are not stored.

### Challenges we ran into

The hardest part was making the system feel like a real agent workflow instead of a generic chatbot. We needed the agents to produce concrete artifacts: DSO email drafts, landlord verification messages, scam checklists, I-94 reminders, official links, budget plans, and risk summaries. That required strict schemas, source validation, and deterministic fallbacks so the system would not invent school policies or immigration advice. Another challenge was balancing safety with usefulness. International student arrival planning touches visa, housing, banking, and school policy. LandingMate avoids legal advice and instead gives source-backed handoffs, verification prompts, and messages students can send to official offices. The payment flow was also challenging because the free workflow still needed to complete cleanly while a premium agent remained locked. We built the orchestration so premium agents can participate in the workflow as locked responses, then return full output after payment verification.

### Accomplishments we're proud of

We built a complete ASI:One-compatible multi-agent workflow that turns one message into an actionable arrival pack. We are especially proud of: A public Agentverse/ASI:One orchestrator. Multiple specialist agents with clear responsibilities. Official-source handoffs instead of generic advice. Ready-to-send DSO and landlord messages. Housing scam prevention artifacts. Timeline and risk aggregation. Payment-gated premium agent behavior. Privacy-first design with no user accounts or persistent personal database. Deterministic fallback logic so the demo does not depend on LLM availability. ##

### What we learned

We learned that agent systems are strongest when each agent owns a specific decision surface. A "student assistant" is too broad, but a visa check-in agent, housing safety agent, banking prep agent, campus intelligence agent, risk agent, and timeline agent can coordinate into something useful. We also learned that safety matters more than fluency in this domain. For international students, a confident hallucination is worse than no answer. LandingMate therefore focuses on official links, verification prompts, and user-confirmable artifacts. Finally, we learned that payments make agents feel more like services. Locking and unlocking CampusVibeAgent helped us model how specialized agents could be monetized inside an ASI:One workflow.

### What's next

Next, we want to expand LandingMate into a full international student arrival operating system. Planned features include more schools and official international office integrations, real appointment and deadline reminders, more robust housing listing analysis, student-to-student verified community signals, F-1/J-1 document upload parsing, multi-language support, World/identity-based student verification, deeper Fetch.ai Payment Protocol support for premium specialist agents, and a full mobile arrival checklist experience. Long term, LandingMate should let any incoming international student describe where they are going and receive a safe, verified, step-by-step arrival control room in minutes.

## README (from the GitHub repository)

# LandingMate AI

An arrival-coordination assistant for international students that turns an I-20 (or a short chat message) into a checklist of concrete next steps — each one carrying the official source it came from.

![Agent results with per-task source attribution](docs/assets/agent-results.png)

## The problem

People act on this output. A student reads "open your school's check-in handoff" and clicks the link. They read a date and book a flight around it. They read "send this to your DSO" and send it.

That makes a fabricated source URL or an invented deadline categorically worse than a mediocre answer. A vague checklist wastes someone's afternoon; a hallucinated check-in link sends them to a page that isn't their school's, and a hallucinated deadline makes them miss a real one.

So the system is built on one assumption:

> **The LLM is not trusted to hold facts. It is trusted only to organize language.**

Every URL, date, priority, and timing bucket in the output is produced by deterministic Python before any model is called. The model's only permitted contribution is rewording two prose fields — `TaskItem.task` and `TaskItem.why_it_matters` — without introducing a link. The backend verifies that's all it did before the output ships. If verification fails for any reason, the deterministic version is what the user sees.

## Architecture

```mermaid
flowchart TD
    A1["I-20 PDF · regex extraction"] --> SR
    A2["Chat message · LLM + regex override"] --> SR
    SR["SchoolResolver — fuzzy match ≥ 0.72"] --> EP["Enriched profile + official school URLs"]
    EP --> B

    subgraph agent["Each of 4 DomainAgents · domain_agent.py"]
        B["Deterministic baseline<br/>tasks · timing · priority · source URLs · risks"] --> G{"USE_LLM_AGENTS"}
        G -->|"false — default"| OUT["Final response"]
        G -->|true| L["Claude refinement · wording fields only"]
    end

    subgraph guard["Guardrails · _validate_refinement()"]
        V1["Schema parse — Literal-typed enums"] --> V2["Field equality vs baseline"]
        V2 --> V3["Source URL allowlist"]
    end

    L --> V1
    V1 -->|invalid| FB
    V2 -->|mismatch| FB
    V3 -->|not allowlisted| FB
    L -->|API or network error| FB["Discard refinement · keep baseline"]
    V3 -->|all pass| OUT
    FB --> OUT

    OUT --> FA["FinalAdvisor · final_advisor.py"]
    FA --> UI["Action pack — handoffs · drafts · checklists · risks · sources"]
```

Four DomainAgents run concurrently: `VisaCheckinAgent`, `BankingPrepAgent`, `HousingSafetyAgent`, `CampusVibeAgent` ([`domain_agent.py:23`](backend/app/agents/domain_agent.py#L23)). All three runtime paths in this repo — the local async runtime, the in-process agent-messaging runtime, and the separately registered specialist agents — call the same `run_domain_agent()`, so the guardrails below apply identically no matter how a request arrives.

## How the guardrails work

The refinement contract lives in two places: [`domain_agent.py:103-112`](backend/app/agents/domain_agent.py#L103) decides *whether* to call the model, and [`_validate_refinement()`](backend/app/llm/anthropic_client.py#L270) decides whether to *keep* what came back.

The contract in one sentence:

> The model may rewrite `TaskItem.task` and `TaskItem.why_it_matters`, and nothing else. Any other difference from the baseline — in any field, at any level, including one this code does not anticipate — discards the entire refinement and returns the deterministic baseline unchanged.

It is a whitelist, not a blacklist. Immutable `TaskItem` fields are compared by iterating `TaskItem.model_fields` ([`anthropic_client.py:253`](backend/app/llm/anthropic_client.py#L253)) rather than by an enumerated list, so a field added to the schema later is immutable by default instead of silently unguarded.

Each claim below links to the code that enforces it and to the test that proves it. The suite is 69 offline tests in [`tests/test_refinement_guardrails.py`](backend/tests/test_refinement_guardrails.py), run on every push and pull request by [`.github/workflows/ci.yml`](.github/workflows/ci.yml).

### 1. The deterministic baseline always exists first

`_build_deterministic_domain_response()` ([`domain_agent.py:115`](backend/app/agents/domain_agent.py#L115)) builds the complete response — task text, timing bucket, priority, source title, source URL, handoff URL, draft messages, risk items — in plain Python before any model call. The LLM is handed a finished object and asked to improve it, never asked to produce one.

Applies to all four agents. When `USE_LLM_AGENTS=false` (the default, [`config.py:15`](backend/app/config.py#L15)) no model is involved in the plan at all.

### 2. Only two prose fields per task may change

Every `TaskItem` field except `task` and `why_it_matters` must equal the baseline: `timing_bucket`, `priority`, `source_title`, `source_url`, `confidence`, `verify_with`, `action_type`, `artifact_title`, `artifact_body`, `handoff_url`, `draft_message`, `requires_user_confirmation` ([`anthropic_client.py:251`](backend/app/llm/anthropic_client.py#L251)). Any difference discards the whole response.

*Tests:* one case per immutable field, plus `test_every_task_field_except_the_two_mutable_ones_is_guarded`, which fails if a new schema field is ever added without a decision about its mutability.

### 3. Risk items, summaries, and payment fields are frozen

`risk_items` is compared as a whole list ([`anthropic_client.py:284`](backend/app/llm/anthropic_client.py#L284)). Because Pydantic equality is field-by-field, that single comparison pins risk count, risk order, and every `risk`, `severity`, and `related_task` value. `outcome_summary`, `premium_required`, `payment_status`, `price_usd`, `request_id`, `agent_name`, and `domain` are checked against the baseline by name ([`anthropic_client.py:213`](backend/app/llm/anthropic_client.py#L213)).

The model cannot re-rank a risk from `high` to `low`, reword a risk, reorder the list, rewrite the summary, or touch a payment field.

*Tests:* per-field cases for all three `RiskItem` fields, risk reordering, add/remove for both lists, and per-field cases for every immutable response field.

### 4. No new links, including in prose

`source_url` and `handoff_url` must equal the baseline, so the model cannot add, swap, or retarget a link *field*. Beyond that, the two mutable prose fields are scanned: `_introduces_url()` ([`anthropic_client.py:241`](backend/app/llm/anthropic_client.py#L241)) extracts URL-like substrings from the baseline text and the candidate text and rejects the refinement if the candidate contains one the baseline did not.

The detector catches explicit schemes (`https://…`), `www.` hosts, markdown links, and bare domains on common TLDs. A link the deterministic layer already wrote may survive a rewrite; anything new is treated as invention. The refinement is rejected outright rather than having the link stripped, so a model that tries this gets no partial credit.

**Boundary worth stating:** this is a regex heuristic, not a parser. It is tuned to reject the realistic failure — a model appending a plausible-looking check-in URL — while leaving ordinary prose alone (`I-20`, `U.S.`, `document_parser.py`, `schools.json` are all tested as non-matches). An adversarial obfuscation such as `example dot com` would pass it. That is an acceptable residual risk here, because the model is not adversarial and the field renders as plain text rather than as a clickable link.

*Tests:* six injection forms in `task`, four in `why_it_matters`, swapping a pre-existing URL for a different one, plus direct unit tests of the helper against both link forms and ordinary prose.

### 5. Timing and priority are structurally immutable

Beyond the equality check, `TimingBucket` and `Priority` are `Literal` types ([`schemas.py:8-9`](backend/app/schemas.py#L8)), so an invented value like `"urgent"` or `"2_weeks_before"` fails Pydantic parsing before the comparison logic is reached. 

[README truncated for size]

## Detected evidence (automated analysis)

Indexed codebase: 41 recognized source files, 147 KB.
- Anthropic (technology) — detected in the code
- CSS (language) — detected in the code
- FastAPI (technology) — detected in the code
- HTML (language) — detected in the code
- JavaScript (language) — detected in the code
- Python (language) — detected in the code
- React (technology) — detected in the code
- Tailwind CSS (technology) — detected in the code
- TypeScript (language) — detected in the code

## Codebase structure (from repository index)

### Files (52 of 52)

```
.env.example
.gitignore
backend/agentverse_orchestrator.py
backend/app/__init__.py
backend/app/agents/domain_agent.py
backend/app/agents/final_advisor.py
backend/app/agents/function_runtime.py
backend/app/agents/report_formatter.py
backend/app/agents/uagent_runtime.py
backend/app/agents/uagent_wrappers.py
backend/app/config.py
backend/app/llm/__init__.py
backend/app/llm/anthropic_client.py
backend/app/main.py
backend/app/payments/__init__.py
backend/app/payments/catalog.py
backend/app/payments/payment_protocol.py
backend/app/payments/stripe_service.py
backend/app/schemas.py
backend/app/services/data_loader.py
backend/app/services/document_parser.py
backend/app/services/reddit_client.py
backend/app/services/school_resolver.py
backend/app/services/term_parser.py
backend/data/demo_profiles.json
backend/data/demo_student_profile.json
backend/data/schools.json
backend/data/trusted_sources.json
backend/Makefile
backend/mas/__init__.py
backend/mas/agent_config.py
backend/mas/banking_agent.py
backend/mas/campus_agent.py
backend/mas/housing_agent.py
backend/mas/mas_models.py
backend/mas/orchestrator_agent.py
backend/mas/subagent_base.py
backend/mas/visa_agent.py
backend/requirements.txt
backend/scripts/make_synthetic_profile_pdf.py
backend/scripts/make_test_i20_full.py
frontend/index.html
frontend/package.json
frontend/postcss.config.js
frontend/src/main.tsx
frontend/src/styles.css
frontend/src/vite-env.d.ts
frontend/tailwind.config.js
frontend/tsconfig.json
frontend/tsconfig.tsbuildinfo
frontend/vite.config.ts
README.md
```

### Dependencies

- backend/requirements.txt: anthropic@==0.71.0, fastapi@==0.115.6, pdfplumber@==0.11.5, pydantic@==2.10.4, PyMuPDF@==1.25.1, python-multipart@==0.0.20, stripe@==14.0.0, uagents@==0.22.10, uagents-core@==0.3.11, uvicorn[standard]@==0.34.0
- frontend/package.json: @types/react@^19.2.17, @types/react-dom@^19.2.3, @vitejs/plugin-react@latest, autoprefixer@latest, lucide-react@latest, postcss@latest, react@latest, react-dom@latest, tailwindcss@^3.4.17, typescript@latest, vite@^8.0.16

### Recent commits (newest first)

- initial snapshot

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

### backend/requirements.txt

```
fastapi==0.115.6
uvicorn[standard]==0.34.0
python-multipart==0.0.20
pydantic==2.10.4
PyMuPDF==1.25.1
pdfplumber==0.11.5
uagents==0.22.10
uagents-core==0.3.11
anthropic==0.71.0
stripe==14.0.0

```

### frontend/package.json

```
{
  "name": "landingmate-ai-frontend",
  "private": true,
  "version": "0.1.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "tsc -b && vite build",
    "preview": "vite preview"
  },
  "dependencies": {
    "@vitejs/plugin-react": "latest",
    "lucide-react": "latest",
    "react": "latest",
    "react-dom": "latest",
    "typescript": "latest"
  },
  "devDependencies": {
    "@types/react": "^19.2.17",
    "@types/react-dom": "^19.2.3",
    "autoprefixer": "latest",
    "postcss": "latest",
    "tailwindcss": "^3.4.17",
    "vite": "^8.0.16"
  }
}

```

### backend/app/main.py

```python
from __future__ import annotations

import json

from fastapi import FastAPI, File, Form, UploadFile
from fastapi.middleware.cors import CORSMiddleware

from app.agents.function_runtime import run_function_runtime
from app.agents.uagent_runtime import run_uagent_runtime
from app.config import AGENT_RUNTIME_MODE, ANTHROPIC_API_KEY, ANTHROPIC_MODEL, DEMO_MODE, ENABLE_AGENT_PAYMENTS, PAYMENT_DEMO_MODE, USE_LLM_AGENTS, USE_REDDIT_LIVE
from app.payments.catalog import catalog_payload
from app.payments.stripe_service import create_checkout_for_agent, verify_agent_payment
from app.schemas import CheckoutRequest, FinalReport, StudentProfile, VerifyPaymentRequest
from app.services.document_parser import parse_i20_pdf
from app.services.school_resolver import resolve_school

app = FastAPI(title="LandingMate AI", version="0.1.0")

app.add_middleware(
    CORSMiddleware,
    allow_origins=["http://localhost:5173", "http://127.0.0.1:5173"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)


@app.get("/health")
async def health():
    return {
        "ok": True,
        "runtime_mode": AGENT_RUNTIME_MODE,
        "demo_mode": DEMO_MODE,
        "llm_agents_enabled": USE_LLM_AGENTS,
        "anthropic_model": ANTHROPIC_MODEL,
        "anthropic_key_configured": bool(ANTHROPIC_API_KEY),
        "agent_payments_enabled": ENABLE_AGENT_PAYMENTS,
        "payment_demo_mode": PAYMENT_DEMO_MODE,
        "reddit_live_enabled": USE_REDDIT_LIVE,
    }


@app.get("/payments/catalog")
async def payments_catalog():
    return {
        "enabled": ENABLE_AGENT_PAYMENTS,
        "demo_mode": PAYMENT_DEMO_MODE,
        "products": catalog_payload(),
    }


@app.post("/payments/checkout")
async def payments_checkout(request: CheckoutRequest):
    return create_checkout_for_agent(request.agent_name)


@app.post("/payments/verify")
async def payments_verify(request: VerifyPaymentRequest):
    return verify_agent_payment(request.agent_name, request.transaction_id)


@app.post("/plan", response_model=FinalReport)
async def create_plan(file: UploadFile = File(...), unlocked_agents: str = Form("[]")):
    pdf_bytes = await file.read()
    unlocked_agent_names = _parse_unlocked_agents(unlocked_agents)
    raw_profile, used_demo_profile, logs = parse_i20_pdf(pdf_bytes)
    enriched_profile, resolver_logs = resolve_school(raw_profile)
    profile = StudentProfile(**enriched_profile)
    logs.extend(resolver_logs)

    if AGENT_RUNTIME_MODE == "uagent":
        try:
            return await run_uagent_runtime(profile, logs, used_demo_profile, unlocked_agent_names)
        except Exception as exc:
            logs.append(f"[Coordinator] UAGENT_MODE failed; falling back to function runtime: {exc}")

    return await run_function_runtime(profile, logs, used_demo_profile, unlocked_agent_names)


@app.post("/clear-session")
async def clear_session():
    return {
        "cleared": True,
        "reset": ["session profile", "report", "logs"],
        "storage": "in-memory only",
    }


def _parse_unlocked_agents(value: str) -> list[str]:
    try:
        parsed = json.loads(value)
    except json.JSONDecodeError:
        return []
    if not isinstance(parsed, list):
        return []
    return [item for item in parsed if isinstance(item, str)]

```

### frontend/src/main.tsx

```typescript
import React, { useEffect, useMemo, useState } from "react";
import ReactDOM from "react-dom/client";
import {
  CreditCard,
  ExternalLink,
  FileText,
  FileUp,
  Lock,
  MessageSquareText,
  RotateCcw,
  ShieldCheck,
  Sparkles,
  Unlock
} from "lucide-react";
import "./styles.css";

const API_BASE = import.meta.env.VITE_API_BASE ?? "http://localhost:8000";
const LANDINGMATE_AGENT_ADDRESS = "agent1qwg0t3w888wfs9ecsm0zywc5fftgnnujx9xm8t2na4vayj84nt6956q2maf";
const ASI_ONE_CHAT_URL =
  import.meta.env.VITE_ASI_ONE_CHAT_URL ?? `https://agentverse.ai/agents/details/${LANDINGMATE_AGENT_ADDRESS}`;

const AGENT_STYLES: Record<string, { band: string; shell: string; chip: string; accent: string; label: string }> = {
  VisaCheckinAgent: {
    band: "bg-sky",
    shell: "bg-sky/15",
    chip: "bg-sky/25",
    accent: "border-sky/80",
    label: "Visa & check-in",
  },
  HousingSafetyAgent: {
    band: "bg-coral",
    shell: "bg-coral/12",
    chip: "bg-coral/25",
    accent: "border-coral/80",
    label: "Housing safety",
  },
  BankingPrepAgent: {
    band: "bg-lemon",
    shell: "bg-lemon/25",
    chip: "bg-lemon/40",
    accent: "border-amber-500/80",
    label: "Banking prep",
  },
  CampusVibeAgent: {
    band: "bg-mint",
    shell: "bg-mint/18",
    chip: "bg-mint/35",
    accent: "border-emerald-500/80",
    label: "Campus vibe",
  },
};

type Task = {
  task: string;
  timing_bucket: string;
  priority: string;
  why_it_matters: string;
  source_title?: string;
  source_url?: string;
  confidence: string;
  verify_with: string;
  action_type?: string;
  artifact_title?: string;
  artifact_body?: string;
  handoff_url?: string;
  draft_message?: string;
  requires_user_confirmation?: boolean;
};

type DomainResponse = {
  agent_name: string;
  domain: string;
  tasks: Task[];
  risk_items: { risk: string; severity: string; related_task: string }[];
  premium_required: boolean;
  payment_status: string;
  price_usd?: string;
};

type AgentProduct = {
  agent_name: string;
  domain: string;
  tier: string;
  price_usd: string;
  amount_cents: number;
  description: string;
};

type Report = {
  status: string;
  student_profile: {
    school: string;
    campus?: string;
    city?: string;
    state?: string;
    program?: string;
    visa_type?: string;
    program_start_date: string;
    estimated_expenses?: number;
    school_resources: Record<string, string | boolean>;
  };
  completed_agents: string[];
  timed_out_agents: string[];
  domain_responses: DomainResponse[];
  summary: string;
  verification_note: string;
  activity_logs: string[];
  runtime_mode: string;
  payment_summary: Record<string, unknown>;
};

function App() {
  const compactMode = new URLSearchParams(window.location.search).has("thumbnail") ||
    new URLSearchParams(window.location.search).has("compact");
  const [file, setFile] = useState<File | null>(null);
  const [report, setReport] = useState<Report | null>(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState("");
  const [products, setProducts] = useState<AgentProduct[]>([]);
  const [unlockedAgents, setUnlockedAgents] = useState<string[]>([]);

  useEffect(() => {
    fetch(`${API_BASE}/payments/catalog`)
      .then((response) => response.json())
      .then((data) => setProducts(data.products ?? []))
      .catch(() => setProducts([]));
  }, []);

  const sources = useMemo(() => {
    if (!report) return [];
    const unique = new Map<string, { title: string; url: string; domain: string }>();
    for (const response of report.domain_responses) {
      for (const task of response.tasks) {
        if (task.source_url && task.source_title) {
          unique.set(task.source_url, { title: task.source_title, url: task.source_url, domain: response.domain });
        }
      }
    }
    return Array.from(unique.values());
  }, [report]);

  const agentResponses = useMemo(() => {
    if (!report) return [];
    const order = ["VisaCheckinAgent", "HousingSafetyAgent", "BankingPrepAgent", "CampusVibeAgent"];
    const byName = new Map(report.domain_responses.map((response) => [response.agent_name, response]));
    return order.map((name) => byName.get(name)).filter((response): response is NonNullable<typeof response> => Boolean(response));
  }, [report]);

  async function submitPlan(uploadFile: File) {
    setLoading(true);
    setError("");
    const formData = new FormData();
    formData.append("file", uploadFile);
    formData.append("unlocked_agents", JSON.stringify(unlockedAgents));
    try {
      const response = await fetch(`${API_BASE}/plan`, { method: "POST", body: formData });
      if (!response.ok) throw new Error(`Backend returned ${response.status}`);
      setReport(await response.json());
    } catch (err) {
      setError(err instanceof Error ? err.message : "Plan generation failed");
    } finally {
      setLoading(false);
    }
  }

  async function clearSession() {
    await fetch(`${API_BASE}/clear-session`, { method: "POST" }).catch(() => null);
    setFile(null);
    setReport(null);
    setError("");
    setUnlockedAgents([]);
  }

  async function unlockAgent(agentName: string) {
    setError("");
    try {
      const checkout = await fetch(`${API_BASE}/payments/checkout`, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ agent_name: agentName })
      }).then((response) => response.json());
      const verification = await fetch(`${API_BASE}/payments/verify`, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ agent_name: agentName, transaction_id: checkout.transaction_id })
      }).then((response) => response.json());
      if (verification.paid) {
        setUnlockedAgents((current) => Array.from(new Set([...current, agentName])));
        return;
      }
      setError(`${agentName} payment was not completed.`);
    } catch (err) {
      setError(err instanceof Error
[truncated — 14269 more characters]
```

### frontend/postcss.config.js

```javascript
export default {
  plugins: {
    tailwindcss: {},
    autoprefixer: {}
  }
};

```

### frontend/vite.config.ts

```typescript
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";

export default defineConfig({
  plugins: [react()],
  server: {
    port: 5173
  }
});

```

### frontend/index.html

```html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>LandingMate AI</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.tsx"></script>
  </body>
</html>

```

### frontend/tailwind.config.js

```javascript
/** @type {import('tailwindcss').Config} */
export default {
  content: ["./index.html", "./src/**/*.{ts,tsx}"],
  theme: {
    extend: {
      colors: {
        ink: "#202124",
        paper: "#fbfaf7",
        mint: "#dff8ec",
        lemon: "#fff2a8",
        coral: "#ffb7a7",
        sky: "#ddecff"
      },
      boxShadow: {
        sketch: "0 10px 0 rgba(32, 33, 36, 0.08)"
      }
    }
  },
  plugins: []
};

```

### backend/agentverse_orchestrator.py

```python
from __future__ import annotations

import os
import re
from datetime import datetime, timezone
from uuid import uuid4

from uagents import Agent, Context, Protocol
from uagents_core.contrib.protocols.chat import (
    ChatAcknowledgement,
    ChatMessage,
    EndSessionContent,
    TextContent,
    chat_protocol_spec,
)
from uagents_core.contrib.protocols.payment import CommitPayment, CompletePayment, Funds, RejectPayment, RequestPayment

from app.agents.function_runtime import run_function_runtime
from app.agents.report_formatter import format_arrival_action_pack
from app.config import PAYMENT_DEMO_MODE, STRIPE_PUBLISHABLE_KEY, STRIPE_SECRET_KEY
from app.payments.catalog import product_for_agent
from app.payments.payment_protocol import build_payment_proto
from app.payments.stripe_service import create_checkout_for_agent, verify_agent_payment
from app.schemas import StudentProfile
from app.services.document_parser import normalize_date
from app.services.school_resolver import resolve_school
from app.services.term_parser import extract_start_window, has_explicit_date


AGENT_NAME = "LandingMateOrchestratorAgent"
AGENT_PORT = int(os.getenv("LANDINGMATE_AGENT_PORT", "8012"))
AGENT_SEED = os.getenv("LANDINGMATE_AGENT_SEED", "landingmate-orchestrator-ucb-ai-2026-stable-v1")
PREMIUM_AGENT = "CampusVibeAgent"

paid_senders: set[str] = set()
profile_state: dict[str, dict] = {}

agent = Agent(
    name=AGENT_NAME,
    seed=AGENT_SEED,
    port=AGENT_PORT,
    mailbox=True,
    publish_agent_details=True,
    log_level="INFO",
)

chat_proto = Protocol(spec=chat_protocol_spec)


@chat_proto.on_message(ChatMessage)
async def handle_chat(ctx: Context, sender: str, msg: ChatMessage):
    await ctx.send(sender, ChatAcknowledgement(acknowledged_msg_id=msg.msg_id))
    user_text = msg.text().lower()
    original_text = msg.text()
    if _is_new_session_greeting(original_text):
        profile_state[sender] = {}
    profile_state[sender] = _merge_profile(profile_state.get(sender, {}), original_text)

    if _wants_payment(user_text):
        missing = _missing_required_fields(profile_state.get(sender, {}))
        if missing:
            await _send_text(ctx, sender, _intake_question(profile_state.get(sender, {}), missing))
            return
        if PAYMENT_DEMO_MODE:
            paid_senders.add(sender)
            profile_state[sender] = _default_profile() if _missing_required_fields(profile_state.get(sender, {})) else profile_state[sender]
            try:
                report = await _run_landingmate_workflow(profile_state[sender], [PREMIUM_AGENT])
                await _send_text(ctx, sender, "Demo unlock accepted. CampusVibeAgent unlocked.\n\n" + _format_report(report), end_session=True)
            except Exception:
                ctx.logger.exception("demo premium workflow failed after unlock")
                await _send_text(ctx, sender, "Demo unlock accepted, but I hit an error re-running the premium report.", end_session=True)
            return
        await _send_payment_request(ctx, sender)
        return

    missing = _missing_required_fields(profile_state.get(sender, {}))
    if missing:
        await _send_text(ctx, sender, _intake_question(profile_state.get(sender, {}), missing))
        return

    try:
        report = await _run_landingmate_workflow(profile_state[sender], [])
        await _send_text(ctx, sender, _format_report(report), end_session=True)
    except Exception:
        ctx.logger.exception("LandingMate workflow failed")
        await _send_text(
            ctx,
            sender,
            "I hit a problem building your plan (a data source or tool call may have failed). "
            "Please try again in a moment, or resend your school and program start date.",
            end_session=True,
        )


@chat_proto.on_message(ChatAcknowledgement)
async def handle_ack(ctx: Context, sender: str, msg: ChatAcknowledgement):
    ctx.logger.info("Chat acknowledgement received from %s for %s", sender, msg.acknowledged_msg_id)


async def on_commit(ctx: Context, sender: str, msg: CommitPayment):
    if msg.funds.payment_method != "stripe":
        await ctx.send(sender, RejectPayment(reason="LandingMate currently accepts Stripe payments for premium agents."))
        return
    demo_payment = PAYMENT_DEMO_MODE or not STRIPE_SECRET_KEY or not STRIPE_PUBLISHABLE_KEY
    if not demo_payment:
        verification = verify_agent_payment(PREMIUM_AGENT, msg.transaction_id)
        if not verification.paid:
            await ctx.send(sender, RejectPayment(reason="Stripe payment has not been completed yet."))
            return

    paid_senders.add(sender)
    await ctx.send(sender, CompletePayment(transaction_id=msg.transaction_id))
    if demo_payment:
        await _send_text(ctx, sender, "Demo payment accepted. CampusVibeAgent unlocked.")
    if _missing_required_fields(profile_state.get(sender, {})):
        profile_state[sender] = _default_profile()
    try:
        report = await _run_landingmate_workflow(profile_state[sender], [PREMIUM_AGENT])
        await _send_text(ctx, sender, "Payment verified. CampusVibeAgent unlocked.\n\n" + _format_report(report), end_session=True)
    except Exception:
        ctx.logger.exception("LandingMate premium workflow failed after payment")
        await _send_text(
            ctx,
            sender,
            "Payment verified and CampusVibeAgent unlocked, but I hit an error generating the premium report. "
            "Please send your school and start date again to retry.",
            end_session=True,
        )


async def on_reject(ctx: Context, sender: str, msg: RejectPayment):
    await _send_text(ctx, sender, f"Payment was not completed: {msg.reason or 'cancelled'}. You can still run the free arrival plan.")


async def _send_payment_request(ctx: Context, sender: str):
    checkout = create_checkout_for_agent(PREMIUM_AGENT)
    product = product_for_agent(PREMIUM_AGENT)
    if product is None:
        await _send_tex
[truncated — 7400 more characters]
```

### backend/app/__init__.py

```python


```

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