The stumble is the calibration

In March I started building JobFilter as a hosted career platform. Upload your CV, we parse it, we match you against jobs, we write your cover letter, you pay with Stripe. The full product, not a sketch: CV parsing with a human-in-the-loop acceptance flow, deterministic matching with typed evidence, a ranked feed with save and dismiss, application tracking, cover-letter generation with versioning and PDF export, quota enforcement, an admin console, GDPR data export. Phases A through H, all shipped. I even built the compliance machinery — audit logging, prompt-injection mitigation, AI budget guards — because I knew from day one that career data is the sensitive kind.

In July I deleted roughly sixty-six thousand lines of it and turned the product into a public-job intelligence API for agents.

That's the stumble. A stumble, done right, is a calibration: it tells you where the ground actually is. And I've been careful about how to frame it, because the standard framing — "we built the wrong thing" — is what makes pivots look like trend-hopping. The career platform wasn't a wrong guess we could have avoided by thinking harder. It was the experiment that produced the data. The decision tree is the story.

Node one: the moat. I'd assumed the product was the matching — the AI that reads a CV and ranks jobs against it. That assumption was the stumble: the thing that turned out to be genuinely hard, and genuinely defensible, was job supply — fresh, canonical, deduplicated jobs with source coverage, freshness tracking, enrichment, and quality gates. Matching is a commodity; every platform that already owns distribution can ship it in a sprint. The corpus is the asset.

Node two: the compliance cost. Hosted career data sits in GDPR Article 9 territory — a CV can contain incidental special-category data, so every byte of a hosted career product drags a compliance program behind it: provider contracts, residency, retention, provenance, access, export, erasure, audit evidence. That cost is disproportionate before anyone has demonstrated demand for a hosted profile-and-matching experience.

Node three: defensibility. Set the compliance cost aside and the picture doesn't improve: the features I was proudest of were exactly the ones a platform that already owns distribution could clone trivially — LinkedIn, the job boards, the ATS vendors. We'd have been renting someone else's moat at the price of their compliance burden.

When three independent signals point the same way, the decision stops being a bet and becomes a reading. I didn't reroute to agent infrastructure because agents were hot. I rerouted because the career product was the wrong place to put the moat, and the corpus — the one layer that had proven durable — is precisely what an agent needs: normalized public jobs, fresh and deduplicated, served without ever seeing a CV.

The reroute, concretely: JobFilter now owns ingestion, normalization, canonical IDs, deduplication, freshness, source attribution, and enrichment — the C0 public-job layer.

The search contract is structured JSON only: bounded arrays of roles, skills, locations, remote types, employment types, and languages, plus freshness and pagination. No prose field. Unknown fields fail validation.

// request: bounded filters, no prose
{
  "roles": ["frontend"],
  "skills": ["typescript"],
  "locations": ["Stockholm"],
  "remote_types": ["hybrid", "remote"]
}

// response: source-attributed records, cursor pagination
{
  "items": [
    {
      "canonical_job_id": "1b4e28ba-5ce1-4a24-9d0e-2f3c6a7b8d9e",
      "title": "Frontend Engineer",
      "company_name": "Example AB",
      "location": "Stockholm",
      "remote_type": "hybrid",
      "employment_type": "full_time",
      "language": "en",
      "summary_text": "Build the checkout experience for Sweden's fastest-growing retail platform.",
      "required_skills": ["react", "typescript"],
      "preferred_skills": ["tailwindcss"],
      "apply_url": "https://example.se/jobs/frontend-engineer",
      "posting_url": "https://boards.eu.greenhouse.io/example/jobs/1234",
      "source_name": "greenhouse",
      "source_type": "api",
      "source_country_code": "SE",
      "first_seen_at": "2026-08-08T22:14:00Z",
      "last_seen_at": "2026-08-09T06:00:00Z",
      "published_at": "2026-08-07T09:00:00Z",
      "freshness_signal": "fresh",
      "privacy_class": "C0_public_job_intelligence"
    }
  ],
  "next_cursor": "25",
  "limit": 25,
  "attribution": "Preserve source_name plus posting_url or apply_url when presenting results.",
  "privacy_note": "C0 public job intelligence only. Rank against user-held career data locally."
}

A client connects with an account-scoped token limited to a single jobs:read scope, reads source-attributed job records through REST or MCP, and does the personal ranking locally, on the user's own career context. The landing now reads: Job intelligence for agents that keep users in control.

I'm not going to pretend the deleted code was worthless.

The matching engine — five thousand lines of pure, well-tested scoring logic — was retired because the product moved to a public corpus. The code wasn't the mistake; the belief was — the belief that matching was the layer worth defending. Deleting it was right, and the deletion was a decision about where the moat lives, not a confession.

What reads as trend-hopping is a pivot without a trail: you were X, now you're Y, and the only evidence is that Y is fashionable. Ours has a trail. The architecture decision records document the whole tree — the privacy-first direction I assessed and superseded, the agent-first MVP, the hosted-matching plan I considered and rejected, the corpus API I shipped. Each one has its status and its supersession link. The reroute isn't a leap between bandwagons — it's a path with markers, and every marker carries a reason.

A day after the last of the career code was pulled from the tree, semantic retrieval over the public corpus was in production. The layer that survived the deletion was already paying dividends. That's the kind of signal no pitch deck can give you.

The API is live at jobfilter.ai — if you're building agents, that's the corpus to point them at.