Project Info
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.
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.

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
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). 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 decides whether to call the model, and _validate_refinement() decides whether to keep what came back.
The contract in one sentence:
The model may rewrite
TaskItem.taskandTaskItem.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) 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, run on every push and pull request by .github/workflows/ci.yml.
1. The deterministic baseline always exists first
_build_deterministic_domain_response() (domain_agent.py:115) 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) 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). 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). 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).
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) 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), so an invented value like "urgent" or "2_weeks_before" fails Pydantic parsing before the comparison logic is reached. Two independent layers.
6. The returned payload is schema-validated
The model's raw text is parsed with DomainResponse(**json.loads(text)) (anthropic_client.py:189). Malformed JSON, missing required fields, wrong types, and out-of-enum values all raise before the object exists.
Tests: invalid JSON and schema-violation cases against a fake client.
7. Source URLs are checked against a data-file allowlist
The allowlist is assembled per request (anthropic_client.py:151-153) from sources_for_domain(domain) — the 19 entries in data/trusted_sources.json — plus the resolved school's own URLs, which SchoolResolver pulls from the 12 entries in data/schools.json. Any source_url outside that set discards the response (anthropic_client.py:261).
Two honest notes. First, this is defense in depth rather than the primary defense: check 2 already rejects any changed source_url, so in practice the allowlist only ever validates URLs that came from the baseline. It is the layer that would still hold if the equality rule were relaxed, and the test suite exercises it directly by passing an allowlist that excludes the baseline URL. Second, when SchoolResolver finds no school above its 0.72 similarity threshold it synthesizes a Google search URL as the fallback resource (school_resolver.py:37) — so for unrecognized schools the allowlist legitimately contains a generated google.com/search URL, and the UI marks that path confidence: low.
8. Every failure lands on the deterministic output
There is no partial-acceptance path and no retry. A missing API key, an import failure, a network error, a JSON parse failure, a schema violation, or any single field mismatch all return baseline unchanged (anthropic_client.py:143, :191, and every return baseline in the validator). The user always receives a complete, source-backed plan; the model can only ever have made it read better.
Tests: LLM disabled, missing API key, import failure, API error, invalid JSON, and schema violation each assert the baseline object is returned.

The allowlist, rendered. Every source link in the action pack resolves to one of these entries.
Testing with synthetic documents
An I-20 is an immigration document. This repo therefore contains no real ones, and the parser is built so that it never needs one.
The three fixtures in backend/assets/ are generated, not redacted. demo-i20.pdf is stamped "TEST FIXTURE · SIMULATED I-20 · ALL DATA FICTIONAL" on the page itself, carries the non-issuable SEVIS ID N0000000000, and names a fictional student. The generators that produce them are checked in alongside them:
backend/scripts/make_test_i20_full.py— full-page synthetic I-20 with the fixture bannerbackend/scripts/make_synthetic_profile_pdf.py— minimal profile PDF for parser tests
Anyone can regenerate the fixtures instead of asking for a real document, and a fixture that leaks into a screenshot or a log is inert.
The parser reinforces this. parse_i20_pdf() (document_parser.py:71) extracts exactly ten fields — school, campus, city, state, program, visa type, start/end dates, precision, estimated expenses. It never reads the student's name or SEVIS ID, because nothing downstream needs them. Uploaded bytes are parsed in memory and never written to disk. The UI states this at the upload control, and the backend exposes POST /clear-session to drop session state.
Stack
Backend — Python 3, FastAPI, Pydantic v2, PyMuPDF with a pdfplumber fallback for text extraction, anthropic for the optional refinement layer.
Frontend — React 19 + TypeScript, built with Vite 8, styled with Tailwind CSS 3. A single-page client in frontend/src/main.tsx.
Run it locally
Backend, from the repo root:
cd backend && python3 -m venv .venv && .venv/bin/pip install -r requirements.txt
cd backend && .venv/bin/uvicorn app.main:app --reload --port 8000
Frontend, in a second terminal:
cd frontend && npm install && npm run dev
Open http://localhost:5173, upload backend/assets/demo-i20.pdf, and click Generate. No API key is required — with the default USE_LLM_AGENTS=false the whole pipeline runs deterministically. To exercise the refinement layer, copy .env.example to .env, set ANTHROPIC_API_KEY, and set USE_LLM_AGENTS=true.
Check the backend independently with curl -s localhost:8000/health, which reports the active runtime mode and which optional layers are enabled.
Testing
The guardrail suite runs fully offline. It builds DomainResponse objects directly and calls the validator, and the few tests covering the Anthropic call path substitute a fake client — no test reaches Anthropic, Stripe, or Reddit, and no API key is needed.
Backend:
cd backend
python -m pip install -r requirements.txt -r requirements-dev.txt
python -m pytest -q
Frontend (npm run build runs tsc -b first, so type errors fail the build):
cd frontend
npm ci
npm run build
Both are run on every push and pull request by .github/workflows/ci.yml.
Scope and limitations
This is a demo, and the boundaries are deliberate rather than unfinished:
- No accounts, no login, no authentication. Any caller can hit any endpoint.
- No database. There is no persistence layer of any kind.
- State is process memory only. Session state lives in module-level dicts; a restart clears everything. This is also why there is no multi-user isolation — the design assumes one demo session at a time.
- Payments run in demo mode. The premium-agent unlock issues a synthetic transaction ID and no money moves. Real card processing is wired but off by default and has never been exercised against a live account. In demo mode a client can still mint its own unlock token — closing that requires a server-side record of issued transactions, which needs the datastore listed above.
- Not deployed. Localhost only. No hosting, no domain, no container. CI builds and tests the code; it does not ship it anywhere.
- Test coverage is targeted, not broad. The guardrail validator is covered thoroughly because it is the security-relevant boundary. The PDF parser, school resolver, payment flow, and API routes have no automated tests and are still verified by hand.
- 12 schools are resolvable. Everything outside
schools.jsonfalls back to a generated search URL flaggedconfidence: lowrather than an official check-in link. - Not advice. The output is a coordination and verification aid, not legal, immigration, housing, tax, or school-policy advice. Every task carries a
verify_withfield saying so.
Origin
LandingMate was built for a hackathon track, which shaped several things that are still visible in the tree:
- Agent framework. The project uses Fetch.ai's uAgents.
backend/mas/holds a distributed variant where each specialist is its own registered agent communicating over uAgent messaging, anduagent_runtime.pyruns the same four agents inside a localBureau.AGENT_RUNTIME_MODEselects between those and the default async runtime; all three sharerun_domain_agent(), so this is a transport choice rather than a logic fork. - Per-agent payments.
CampusVibeAgentis gated at $1.00 to demonstrate per-agent monetization (payments/).PAYMENT_DEMO_MODE=trueis the default and issues synthetic transaction IDs; a Stripe embedded-checkout path exists behind that flag but has never been run against a live account. - Reddit is the one live external data source, feeding
CampusVibeAgentand clearly labeled anecdotal in the output (reddit_client.py).

The guardrail architecture described above was not a hackathon requirement. It came from the observation that this particular product cannot ship a plausible-sounding wrong answer, and it is the part of the project worth reading.
Analysis
View
Metric
No commits on this project resolved to a GitHub account.
Technology
- AnthropicIn code
- CSSIn code
- FastAPIIn code
- HTMLIn code
- JavaScriptIn code
- PythonIn code
- ReactIn code
- Tailwind CSSIn code
- TypeScriptIn code
9 of 9 appear in the indexed code.
AI coding agents
No AI coding agent signals were found in this repository.
Detected from committed agent config files and commit authorship. Absence of a signal is not proof an agent was unused.
Codebase size
Source size
147 KB
Source files
41
Counts recognized source files only; vendored directories, binaries and lockfiles are excluded, so this is smaller than the repository on disk.
Repository
96528025/landingmate
57 files · 748 KB · @ 5952934
Structure
Interface
22 files · 39%Screens, components and styles rendered to the user.
Application logic
18 files · 32%Domain rules, services and shared utilities.
Supporting
Layers are inferred from where files sit in the tree, not from reading the code. A project that names its directories unconventionally will read oddly here — open the file browser to check anything the diagram implies.
Languages
- Python80%
- TypeScript14%
- Markdown4%
- CSS1%
- JavaScript0%
- HTML0%
Share of indexed source by file size. Binary and vendored files are excluded.
Dependencies
frontend/package.json
npm · 11- @vitejs/plugin-react
- lucide-react
- react
- react-dom
- typescript
- +6 more
backend/requirements.txt
pypi · 10- anthropic
- fastapi
- pdfplumber
- pydantic
- PyMuPDF
- python-multipart
- stripe
- uagents
- uagents-core
- uvicorn[standard]
Declared in the repository’s manifests at the indexed commit. A declared package is not proof it is used, and runtime dependencies are listed first.
This project’s features have not been analysed yet.
Export this project's context (description, README, evidence, key source files) to chat with an AI agent elsewhere.