Every buyer project on UpStage belongs to a content vertical. The platform’s matching engine
rewards projects that land in the bucket most relevant to the talent’s declared experience — and the
operator is the one who locks that bucket in. Vertical tagging is the operator-side queue at
/admin/tags, backed by a Postgres project_vertical enum.
Without vertical tagging, matching is purely semantic: profile embeddings vs project embeddings. Tagging adds an explicit, human-curated axis on top of that — one row, one enum value, one operator decision per project.
The project_vertical enum is a single-valued, “primary-axis” column on
buyer_projects. Legacy rows default to short-film; new projects default the same way
unless the creator explicitly picks another value via POST /projects/new.
short-filmcommercialtheaternew-mediaEach value maps to a market category the talent operator cares about — and each is its own “axis of relevance” in the matching engine. Pick one per project.
The end-to-end workflow that takes a project from “just posted” to “ranked on a talent card” flows through three stages:
/admin/tags, chooses a vertical, hits Save.setProjectVertical(id, vertical) in
db/buyer-projects.js.verticalBoost inside the 40% skills weight at
services/matching.js:69–86.
Admin hits /admin/tags → selects the vertical enum → on
POST /admin/tags/vertical/:id the row gets vertical written via
setProjectVertical → matching engine re-reads on the next cron cycle, the
verticals multi-select array cross-references talent project_types, and the
score surfaces as the category sub-score on the talent card.
The tagger UI at /admin/tags is a deliberately thin admin surface — it lists
every buyer_projects row and exposes a dropdown + Save pair per row, backed by the
project_vertical enum.
The dropdown submits via POST /admin/tags/vertical/:id — gated by
requireAdminSecret at routes/admin.js:1663 so only the operator (with the secret
in the URL or the Authorization header) can alter a project’s vertical.
UpStage carries two parallel “tag” surfaces on the brief and on the talent profile. Both feed the same category match logic but the operator only ever touches the brief-side one.
vertical on buyer_projects
(Postgres enum, default short-film), plus a multi-select verticals
TEXT[] array the buyer can fill at POST /projects/new. Talent matching reads
verticals for the explicit overlap cue.
project_types TEXT[] array on seller_profiles the talent fills
when editing their profile. The match boost fires when at least one of the project’s
verticals labels overlaps a talent project_types label (case-insensitive).
The two arrays meet inside computeMatchScore() at
services/matching.js:69–86 — and the overlap yields the 5% explicit
verticalBoost that sharpens the existing 40% skills weight without changing its headroom.
A vertical tag doesn’t take effect the moment it’s saved — it takes effect when the
AI match engine re-reads the row. The daily ai-talent-match-check cron
(referenced in the CLAUDE.md changelog for 2026-08-12) re-runs the matching
every cron run, while the 3-day cadence ensures notifications fire on the
match_run_counter % 3 == 0 rhythm (corrected in the 2026-07-15 fix).
In practice: an operator hits Save on the tagger screen, the next cron cycle re-evaluates the project against every talent profile, and the affected talents see the new category score on their match card — no manual re-scoring required, no separate “re-tag” pipeline to maintain.