Inspiration
Waterloo students do not lack opportunity. They lack a place where opportunity is computable.
Club pages, Instagram bios, faculty directories, Devpost listings, OpenReview acceptances, Luma calendars, Discord threads, and alumni LinkedIn profiles all describe the same city of work — internships, contests, labs, clubs, people — but they never sit in one structure. A student hunting a Shopify internship has to reconstruct the pipeline by hand: which club alumni landed there, which hackathon projects got noticed, which research term sat on the path. That reconstruction is a graph problem pretending to be a browser-tab problem.
We took the Bloomberg Terminal as the model. Markets already treat the world as a live instrument tape: every quote is a node, every trade is an edge, and the UI is a mosaic of analysis blocks, not a feed. Waterloo deserved the same treatment for student and university opportunity. If you can walk a graph of people, orgs, events, and jobs, you can rank what matters, schedule what is coming, and see the path from here to a target.
The ranking question is old and clean. On a directed graph, a random walk has a stationary distribution π satisfying
πᵀ = πᵀ P
for a row-stochastic transition matrix P. PageRank is that walk with a teleport:
πᵀ = α vᵀ + (1 − α) πᵀ P
where v is a probability vector. For a general search v is uniform. For a student, v is the profile. That one substitution — profile as personalized teleport — is the product.
What it does
Waterloo Terminal is a command-driven mosaic for opportunity discovery.
You type a verb or a question in a permanent command bar. The terminal emits analysis blocks like a receipt: calendar, live feed, ranked search, node table, network graph, goal canvas, profile. You drag those panels into a tiled workspace. Sub-panels nest under the panel that spawned them, so a rabbit-hole stays visually grouped.
Everything on screen is a block. A block is a function
B(A, X, π, θ) ↦ R
over the adjacency / transition structure A, node attributes X, the cached rank vector π, and parameters θ. Search itself is a block. Blocks compose: the output of one is legal input to the next.
What you can do in the workspace:
- Search the graph and see each hit with its stationary mass πᵢ and type path (
person-alumni,org-club,job,competition-hackathon). - Inspect a node in a relational table: fundamentals, extensions, and every incident edge. An alumni history or a club network is a diagnosis view, not a biography page.
- Walk the network in a graph view. A companion 3D Markov visualization shows axiom nodes, pipeline nodes, and the profile distance node in space, with π as the visible weight.
- Grow a goal tree. Roots are abstract — olympiads, "get the internship," research. Object nodes hang as leaves. Drag a node or a whole structure onto the canvas; the terminal auto-links it to the best parent (shared edges, name overlap, type priors) and offers alternate synaptic paths you can prune.
- Schedule from the graph. Event-bearing nodes drop onto the calendar. Signed-in users two-way sync with Apple Calendar over CalDAV, so a hackathon deadline that lives on the graph also lives on the phone.
- Watch a Reuters-style feed. The first hour of an item stays live and raw (internships are the flagship case). Older than one hour, it collapses to a summary.
The profile is not a settings page. It is a first-class analysis block: the teleport vector v that bounds which nodes the walk is allowed to approach.
How we built it
Three layers, built so they can grow independently.
1. Graph and rank. Opportunity is five flat structures: an id ↔ integer index table, a typed directed edge list (s, t, τ, w, e), a type registry of dash-paths with default rank multipliers, a queryable attribute store, and a cached score vector. Type is hierarchical by prefix: person-alumni is a person. Edges are only real-world relations (member_of, alum_of, worked_at, listed_on, accepted_by). Provenance stays on the node.
Ingest rebuilds a sparse matrix and rescores. Vote worth is rank divided by out-degree,
vote(u → v) = (πᵤ / outdeg(u)) · wᵤᵥ
so a NeurIPS accepted_by concentrates mass and a Devpost listed_on among tens of thousands of listings does not. Edge-type weights are a trim tab on top of that. Filters and type priors are diagonal masks applied at rank time, never baked into stored edges. Personalized search replaces the uniform teleport with the profile vector v.
Relatedness is random-walk proximity and shared neighbors. A progression ("how do I get to this internship") is a highest-probability path; intermediate nodes are the steps.
2. Ingest (the Bloomberg pipe). Pull source, keep the raw record, extract, translate, dedup, write the five structures, rescore. Structured feeds — Devpost JSON, OpenReview, iCal / Luma, UW Open Data — map onto object sections with no language-model call. Drupal-family Waterloo pages share one scraper. Other HTML gets a one-time structure builder. Extraction fills fundamentals (fund), relation lists (rel → edges), and overflow. Schema drift between modules is a registered translation shim, not a silent guess. Dedup: strong keys merge; mid-band fuzzy scores become a weighted same_as edge and collapse at matrix build, so duplicates cannot split rank.
3. Surface. The product UI is React 19 + Vite + TypeScript: landing page, auth, and a floating-window mosaic (command bar, calendar, feed, search, node table, graph, goal canvas, profile, saved layouts). The Markov inspector is a separate Three.js scene. Auth and Apple Calendar live on Supabase — Auth, Postgres, Vault-encrypted CalDAV app passwords (never on the public row), Edge Functions for connect / sync / push, and pg_cron for periodic pull. The workspace talks to the graph as if it were already live; the demo seed is Waterloo-shaped (clubs, alumni, internships, contests, Tech Week, Hack the North).
Command resolution is hybrid. A mnemonic (cal, graph, goal) is a keyword dispatch. A muddled sentence is an inquiry: theme matching picks the block set (internship path → search + feed + goals; alumni neighborhood → graph + table).
Challenges we ran into
Scattered sources that refuse to be APIs. WUSA clubs hydrate client-side. LinkedIn and Instagram are effectively closed. Faculty pages, URAs, Putnam archives, and CEMC tables are honest HTML but not one schema. We had to split the world into adapters (JSON / iCal / Drupal family) versus one-shot extractors, and be honest about social: fixture snapshots and user export, not a scraper we would be ashamed of on Monday.
Rank that does not lie. Naive degree or raw listing-count makes Devpost the center of the universe. The interesting objects are the ones other high-rank nodes point at. Getting out-degree normalization, edge-type weights, and same_as collapse to agree took more care than drawing the graph. Wrong merges had to stay splittable; a bad entity-resolution is worse than a missing node.
A UI that can hold a terminal. A Bloomberg-style mosaic is easy to parody and hard to use. We needed: a permanent command bar and a hotkey overlay (mutually exclusive), floating windows that still nest by spawn parent, drag of nodes and whole structures, auto-link that is confident enough to help and humble enough to let you prune, and a calendar that is both a graph sink and a real CalDAV peer. Two-way Apple sync meant principal discovery, ctag / sync-token, encrypted secrets, RLS, and a cron path that cannot leak an app-specific password through the Data API.
Composition as a contract. If every panel is a block, the schemas between blocks have to compose. We ended up with an explicit rule: when two modules almost agree, insert one named translation layer; never coerce units or types in place.
Accomplishments that we're proud of
- A working terminal, not a landing-page mock: command bar, mosaic, seven axiomatic blocks, drag-to-calendar, drag-to-goal-tree with scored auto-link, saved layouts, and a profile that is a teleport, not a bio.
- A ranking story you can inspect. Search results show π. The 3D Markov view lets you click a state and read inflow, out-degree, mean holding time, and why that node is an axiom, a profile bound, or a search-conditioned weight.
- Calendar that is real. Sign in, connect iCloud with an app-specific password, sync both ways. Graph events and phone events occupy the same block.
- A data model that can survive the demo. Five structures, typed edges, prefix types, mask-based personalization, and a documented ingest graph (APIs → extract → translate → dedup → store → PageRank → blocks → mosaic).
- Treating opportunity as linear algebra without hiding it. The UI is warm. The math is not a vibe.
What we learned
Opportunity discovery is an information-retrieval problem with a calendar attached. Once you admit that, a social feed is the wrong primitive. A graph plus a stationary distribution plus a mosaic of blocks is the right one.
We also learned the difference between harvesting and ranking. Harvesting is adapters, overflow, and the discipline to keep raw records. Ranking is π, v, and the refusal to let a high-degree listing site launder importance. Personalized PageRank is not a slogan; it is the statement that the user is a distribution over the same nodes as everyone else.
On the product side: a command bar that emits blocks beats a navigation bar that hides them. Auto-link is only trustworthy if the user can see the reason and cut the edge. Sync is only trustworthy if secrets never sit next to the event rows.
And we learned how much of "campus serendipity" is already a Markov process. Alumni sit at high axiom nodes. Clubs are pipelines. Hackathons are sources. The profile is a distance. The walk was always there. Nobody had written down P.
What's next for Waterloo Terminal
- Live ingest on the demo sources. Devpost, OpenReview, Luma / iCal, UW Open Data, and the Drupal-family scrapers (research centres, URAs, faculty, contests) wired to the five structures so the seed graph is a cache, not the product.
- True matrix rank in the loop. scipy.sparse PageRank on every ingest; GPU later (CuPy / torch) with no schema change. Blocks become real B(A, X, π, θ) functions: masked rank, walk proximity, highest-probability paths, subgraph-similarity link prediction.
- Richer personalization. v from the live profile plus calendar modules — what you already scheduled should raise the prior on related nodes.
- Subscriber bots and self-report. Curated channel listeners, batch-approved inbound data, club and hackathon submission for the same reason they want PR: they want to be on the tape.
- More campuses, same schema. MIT, U of T, Stanford are the same object types and the same edge classes. The type registry grows; the mosaic does not.
- Skin later. Reuters tape is the default feed; other views wait. The terminal stays a terminal.
The long bet is simple. If the walk is honest, the next opportunity is just the next state.
Built With
- anthropic-api
- apple-icloud
- edge-functions
- postgresql
- python
- react
- scipy
- sqlite
- supabase
- typescript
Log in or sign up for Devpost to join the conversation.