Inspiration
Howdy will tell you that a section meets at 8:00 on Friday. It won't tell you what the professor teaching that section is rated by the students who had them, or that the same course under someone else has historically averaged a higher GPA. All of that information exists. It's just split across the course catalog, the university's grade distribution reports, and a few thousand student reviews, and none of those three know the others are there.
So MaroonWay pulls them together and does the obvious thing with them. You pick your courses, type what a good week looks like, and you get back the conflict-free schedules that fit, ranked on the professor ratings and the grade history behind each section.
What it does
You sign in with your @tamu.edu Google account. That is not your NetID: MaroonWay never sees your Howdy login and can't touch your registration.
Then you pick classes and say what you want in plain words, something like "no classes before 9, easier tests, fewer quizzes, nothing at Blinn." Press Build and you get schedules that are conflict-free with at least 20 minutes between classes, each one showing why it landed where it did in the ranking: the rating, the difficulty, the historical GPA, how well the times fit what you asked for.
Any professor on a schedule opens up to their A&M grade history and a summary of what recent students said, one covering how they teach generally and one for what your specific course is like under them.
How we built it
The data pipeline was most of the work and none of the fun. We scraped the A&M course catalog into 5,262 courses and parsed the university's own grade distribution reports into one row per section per term. Matching those instructors to Rate My Professors profiles took fuzzy name matching plus a check against the College Station school ID, and then we pulled every review page for every matched professor at one request per second, caching each page to disk so an interrupted run resumes instead of starting over. We keep only reviews from the past year, since a comment from 2019 is about a different class.
Ranking had an obvious trap in it. A 4.9 from two reviews is not better than a 4.6 from eighty, so every average gets pulled toward the campus mean in proportion to how little evidence sits behind it: (n·rating + 10·prior) / (n + 10). The 10 there is a prior weight we picked and can tune, not something we fit to the data. Rating leads, difficulty breaks ties, and official GPA stays its own signal rather than getting folded into one invented composite. The formula lives in two places, as a Postgres view so scores stay current without a recompute job, and in Python so tests can exercise it, and the two are identical by design.
Preferences were the trickiest part. Free text has to become weights somehow, and we didn't want to ask a generative model for JSON and then write the parser and validator for whatever came back. Every question we actually need answered has a fixed answer set: does this professor quiz often, how hard are the exams, is attendance taken. So the weights come from Jev, TypeSafe AI's non-generative System One model. You send it state plus named, schema-constrained questions, and it answers in one pass with typed values and a confidence, in well under a second, and it cannot answer off-schema. Gemini with a response schema and a crude keyword heuristic sit behind the same Decider protocol as fallbacks, so the thing still runs with no API key at all.
The professor summaries come from Gemini Flash in a resumable batch job behind a token bucket, never inside a request. They land in Postgres as structured fields, so the UI can render "attendance: mandatory" as a chip without parsing English a second time.
The backend is FastAPI on Python 3.13, with async SQLAlchemy and asyncpg over Supabase Postgres. Professor pages are the most-hit and most expensive read we have, so they sit in an in-process LRU cache that does three things a plain dict wouldn't. Eviction follows recency, which means a cache of recently viewed professors isn't a feature sitting on top of the LRU, it's what the LRU already is. Single flight turns twenty simultaneous misses on the same professor into one query rather than twenty. And a missing ID is remembered briefly, so a crawler walking /professors/999999 can't re-query Postgres on every hit. There are 335 tests across the scoring, scheduling, cache and ingest layers.
The frontend is Next.js 16 with the App Router and React 19. Server components by default, server actions for Build and Save, 'use client' only where state genuinely lives, Tailwind v4 themed entirely in CSS. Both halves deploy from a single Render blueprint that passes the shared internal token between the two services, so the browser never sees the API's URL or any of its tokens.
Challenges we ran into
A&M closed its public course API in 2023, and the replacement sits behind NetID and Duo. Meeting times were the one thing none of our data sources could give us, and asking a student for their NetID password was never going to happen. But the student is already logged in. So meeting times arrive through a userscript that runs in their own browser on Howdy's class search, reads the results already on the screen, and posts them to our backend. It previews with a dry run that writes nothing, and imports only once you confirm. It never reads or sends a credential.
Professor identity turned out to be a heuristic rather than a fact. A name plus a department is not a verified person, so every row in the pipeline carries a match status and a confidence, and a blank rating means we don't have one rather than meaning a rating of zero. We would rather show you that uncertainty than launder it into a number that looks clean.
Review text is untrusted input too. It's student writing going into an AI summarizer, so the summarizer summarizes it and ignores anything inside it that reads like an instruction.
Free tiers did a surprising amount of our architecture design. Gemini Pro went paid in April 2026 and the free Flash tier allows somewhere around 10 to 30 requests a minute, which is why summaries are a batch job you can resume instead of a call inside a request. Render's free plan sleeps after 15 idle minutes, so the cache warms a deliberately small number of professors at boot. It also runs one worker on purpose, because a second worker would hold its own separate copy of an in-process cache and cut the hit rate roughly in half. Getting past that means moving the cache to Redis.
One of the bugs was small but instructive. The preference chips in the UI were a client-side default the server had never been told about, so the first schedule of a session got built without them, and it would hand you an 8am class sitting directly underneath a chip reading "No classes before 9 AM." Both sides read the same constant now.
What we learned
Shrinking averages toward a prior is worth doing anywhere sample sizes are uneven, and sample sizes are always uneven. Constraining a model to a schema turned out to be more useful to us than reaching for a bigger model and hoping its JSON parsed. And much of the data work was less about parsing than about deciding what an empty cell was allowed to mean.
What's next
Seat and waitlist counts, prerequisite and degree-plan checking against the catalog data we already have, and a way to hand a built schedule to a friend so two people can end up in the same section.
Built With
- asyncpg
- beautiful-soup
- eslint
- fastapi
- google-gemini
- httpx
- javascript
- next-auth
- next.js
- postgresql
- pydantic
- pytest
- python
- rapidfuzz
- rate-my-professors
- react
- render
- sqlalchemy
- supabase
- tailwindcss
- tampermonkey
- typesafe-jev
- typescript
- uvicorn
- vercel
Log in or sign up for Devpost to join the conversation.