CareerOS
A job search that cannot say anything I have not done. By construction.
Every tool in this space writes your resume for you, and every one of them will quietly inflate a number to make a posting fit. CareerOS is the opposite bet: a pipeline where each sentence on the page must trace to a recorded piece of evidence, and where the thing that catches an overclaim is a gate that refuses it rather than a disclaimer that warns about it.
Watch 10,034 postings narrow → Try the containment gate Backend source Frontend source
FastAPI + Next.js · 281 commits · 457 backend tests · nine job boards
The problem
The tools that write your resume are the ones you can least afford to trust.
A resume generator that invents a plausible figure has not made a small error. It has put a sentence in front of a hiring manager that you will be asked about in a room, and you will not know where it came from. The failure is silent at the moment it happens and expensive weeks later.
What CareerOS does
- Scores a posting against a recorded evidence library, deterministically — no model in the scoring path.
- Selects bullets by marginal coverage of that posting’s requirements, not by per-bullet score.
- Refuses generated wording that reaches past the claim behind it.
- Reports what it did not evaluate, so a ranked list is never mistaken for a complete one.
What it refuses to do
- Nothing auto-submits. It prepares an application and stops; the person applies.
- No scraping of boards whose terms prohibit it — the gap is named on the page instead.
- No invented figures, tools or employers, whoever wrote the sentence.
- No fabricated confidence: an unrecognised requirement is a gap, never a pass.
Two demos, both in your browser
The two decisions worth watching happen at opposite ends.
One narrows ten thousand postings to ten. The other decides whether a single sentence is allowed onto the page. Neither needs a backend — they run entirely in the tab.
Ten thousand postings, in 3D
One real discovery run rendered as it happened: every posting fetched, then eliminated in turn by location, eligibility, title prescreen and evidence score until ten remain. The eliminations are the interesting part — the scoring budget only reaches 600 of them, and the visualisation shows which ones it never looked at rather than pretending the ranking was exhaustive.
Open the pipeline →The containment gate
Type a resume bullet and watch the checks run: invented figures, proper nouns the claim never mentioned, substantive words that smuggle in scope, and seniority escalated past the recorded verb. Pass, review or reject — and the policy is deliberately asymmetric, because a model may not vouch for a claim about someone else’s career while the candidate editing their own history always may.
Try the gate →Discovery
Nine boards, and three named refusals.
Every source is a real, publicly documented, unauthenticated API, except Handshake, which publishes a sitemap of public job pages in its own robots.txt with a schema.org block on each — structured data put out for machines to read, which is the same bargain a board API offers.
The three struck through are not missing — they are declined, and the reason is served to the interface rather than left to look like an oversight. LinkedIn and Wellfound both prohibit automated access to pages that render publicly; Wellfound’s terms name “automated or non-automated harvesting, collection or scraping” specifically. Most of what Wellfound lists is reachable anyway: on a sampled page, 18 of 31 postings were served from the employer’s own Greenhouse or Ashby board, so the way to cover it is to add those employers rather than to crawl it.
Decisions that cost real debugging
Every one of these was a bug first.
An unrecognised requirement is a gap, never a pass
Requirement extraction runs two layers: a canonical vocabulary and open-vocabulary detection of requirement-shaped terms it does not recognise. With only the closed list, anything outside it was not merely unscored but unseen, so it could never be reported as a gap. A mortgage compliance posting requiring HMDA and LOS extracted five generic requirements, matched all five, and scored 98/100 with “no gaps” for a job the candidate was not qualified for.
The budget must be spent on the axis you are sorting by
Full scoring parses whole descriptions, so it cannot run on every posting. Search used to score the first 120 in fetch order and sort those by fit — a ranked list that was not a ranking, with a 98-scoring role sitting unscored at position 452. Sorting by date had the identical defect one layer up: the budget still selected on title fit, so “newest” reordered a fit-shaped subset. 517 postings were from the last day and past the relevance floor; 441 never reached the scorer.
Containment is asymmetric on purpose
The same sentence gets three different outcomes depending on who wrote it. From the assistant, any doubt is fatal. From a model proposing a rewrite, a clean one applies and a questionable one queues for review. From the candidate, failures are warnings rather than vetoes — they know things the evidence file has not caught up with, and refusing their own history would be the wrong call. The text saves, marked unverified, and the resume says so.
The backend knowing something is not the same as the page saying it
This is the failure that kept recurring. The liveness check knew which postings had closed and wrote it down; nothing read it, so eight “ready” applications were against postings that no longer existed. The health endpoint’s source list was hand-maintained and two adapters out of date. A board filter that ran after the scoring budget would have shown 11 of Handshake’s 29 roles and called it all of them. Same shape every time: the system knows, and the surface throws it away.
What is in it
Scoring and bullet selection are deterministic and model-free — a stated design position, not an omission. A language model touches wording only, and every generated sentence passes the same containment gate before it can reach a document. A missing API key produces today’s deterministic resume, not a broken one.