Type in your current or former job in plain English. Get back what your skills actually transfer to, the specific gap to close for a target role, and real open positions in Portland right now.
Built for the Displacement track at the Claude Impact Portland hackathon. Full scope in PRD.MD. Two-person plan in SPLIT.md. API contract in API.md.
No install, no dependencies, no API keys, no build step. Node 20+.
npm start # http://localhost:3000
npm run smoke # in a second terminal: hits every routeThat's the whole setup. Every data source is either bundled or public.
| Part | Status |
|---|---|
| Occupation matching from free text | Local — O*NET's public-domain bulk database (release 29.1), matched against 55,120 alternate job titles |
| Skills for the matched occupation | Local — O*NET importance ratings, 0–100 |
| Related / adjacent occupations | Local — O*NET's Related Occupations table |
| Plain-language "why these overlap" text | Ours — generated by naming the actual shared skills, not a similarity score |
| Skills gap between two occupations | Local — importance diff across O*NET Skills, Knowledge, and Hot Technologies. Upgrades to the CareerOneStop API automatically if credentials are present. |
| Portland job listings | Live — Greenhouse and Lever public job board APIs, fetched per request, filtered to the Portland metro |
| Job → occupation matching | Ours, and crude — board postings carry no SOC codes, so we match on title-token overlap. Good enough to surface real roles; not semantic matching, and we don't claim it is. |
| Growth-sector fallback | Hardcoded — data/growth-sectors.json, from QualityInfo.org Portland Metro data |
services.onetcenter.org returns 401 without a registered account, and
registration is human-reviewed. The same data is published as a public-domain
bulk download with no account at all, so we build data/onet.json from it.
That's strictly better for a live demo: no credentials to leak, no rate limit, no network call that can die on stage, sub-millisecond responses. O*NET publishes roughly annually, so "live" would buy nothing.
npm run onet # rebuild data/onet.json from onetcenter.org
npm run jobs # refresh the offline job snapshotThe job boards genuinely are live — Greenhouse's
boards-api.greenhouse.io/v1/boards/{token}/jobs and Lever's
api.lever.co/v0/postings/{token}?mode=json are both public and
unauthenticated. Add an employer with one line in SOURCES in lib/boards.js.
server.js zero-dep node:http — static files + 4 routes
lib/onet.js local O*NET: search, skills, related occupations
lib/gap.js skills gap via importance diff (no credentials)
lib/careeronestop.js OPTIONAL live gap API, used only if creds exist
lib/boards.js live Greenhouse + Lever fetch, no auth needed
lib/jobs.js title-token match from occupation to open roles
data/onet.json GENERATED (npm run onet) — 879 occupations, 2.7 MB
data/portland-jobs.json GENERATED (npm run jobs) — offline job fallback
data/growth-sectors.json hand-assembled, disclosed
public/index.html one page, one input box
public/app.js fetch + render, no framework
public/styles.css barebones stub — design system lands separately
scripts/build-onet.mjs builds data/onet.json from the public download
scripts/snapshot-jobs.mjs builds the offline job fallback
scripts/smoke.sh hits every route
render.yaml is a Render Blueprint — point Render at the repo and deploy.
No secrets required. There's no build step.
- Job matching is title-keyword overlap, so it surfaces leads, not a filtered shortlist. The UI says so.
- One occupation match at a time. No accounts, no database, no saved sessions.
- O*NET data is release 29.1 and updates about once a year.
- The growth-sector fallback rows still need real QualityInfo figures.