All work

OnePercentJobs · AI job matching · Python · chat-first

A shorter honest digest beats a padded one

OnePercentJobs is a job-hunting copilot for people outside tech: finance, consulting and startup roles in India. Stop searching; five to ten hand-picked roles arrive in your chat every morning, each with one line on why it fits you. The work is in making sure every one of them actually does.

Role

Sole engineer

Stack

FastAPI, Celery, pgvector

Decisions

29 written design records

Tests

180, across 24 files

A job search that comes to you

There is no dashboard to check. You tell it about yourself once, in a chat, and from the next morning the right roles come to you. Everything else happens in the same conversation.

For job seekers

Onboarding as a chat

The basics, plus two short questions that make the matching personal: a project you are proud of, and why this industry.

Your CV, if you like

Upload a PDF or Word CV and its facts are added to your profile.

A morning digest

Five to ten roles before you wake up. Fewer on a thin day, never padded.

The Why Line

One sentence per role on why it fits you, citing only things you actually said.

Talk back

Reply to change your preferences, give feedback on a role or manage your plan. An agent with tools handles it.

On the channel you use

Telegram and a web chat with a magic-link sign-in and the digest by email, with WhatsApp next on the same interface.

Behind it

Nightly harvest

Job boards, YC and company hiring pages, read from sitemaps and ATS feeds, with duplicates folded together.

Admin gate

Digests are built early and held for an optional human look before they go out automatically.

Payments

Razorpay UPI autopay, with signed webhooks handled exactly once.

Job search is hours of scrolling for minutes of signal

Someone looking for a finance or consulting role checks the same five sites every day, wades through the same reposted listings, and applies to whatever looks close enough. In a poll of a few hundred job seekers, searching across many sources came up as the biggest time sink by far.

The obvious product is a feed that ranks jobs for you. The hard part is trust. A list that is mostly right but sometimes sends a fresher a Vice President role does more harm than no list at all, because after the first bad one nobody reads the rest.

How I got there

Three passes, in the order they happened.

Explore

Collect everything, rank by similarity, let a model explain

Scrape every page
Nearest wins
Let it explain

Realise

A live test sent a fresher a senior role, with a confident reason

Scrape every page
Nearest wins
Let it explain

Insight

Cheap sources, hard guards, a skeptical second pass

Read the sitemap
Guard seniority
Verify, then drop

Rules first, model second, and a second model that says no.

From every posting to a handful

Four steps, cheapest first. Each one only sees what the one before it let through, so the expensive model calls are spent on very few postings.

  1. Harvest from sitemaps, not pages

    Job boards are read from their sitemaps and company boards from their ATS JSON. No headless browsers and no anti-bot fights. Postings are fingerprinted on company, title and location so a reposted job is one job.

    app/harvest/

  2. Hard rules in SQL

    Role family, experience band, freshness and whether the listing is still live, all in one query. Unknown values pass; only a known mismatch is a no.

    app/matching/retrieval.py

  3. Retrieve with pgvector

    The 30 to 50 postings closest to your profile, in the same Postgres query as the filters. One database, nothing to keep in sync.

  4. Curate

    A model re-ranks the shortlist against your story and writes a Why Line for each pick. It is told not to pad to a target count.

    app/matching/curation.py

  5. Verify, and drop

    A second, skeptical pass checks each pick against your actual facts and drops it if the seniority, the domain or the reason does not hold. Kept picks get their Why Line rewritten to cite only what you said.

The live test that changed the design

In an early test, a seeker with two years of experience was shown a Senior Software Engineer role and a 12 to 16 year leadership role. The cause was upstream: one source gave a title and nothing else, so experience was blank on every posting, the lenient filter let them all through, and matching fell back to title similarity, which cannot tell junior from senior. Then the model wrote a confident reason for each mismatch.

Three fixes followed. A deterministic guard drops clearly senior titles for junior seekers. The verification pass was added after curation. And postings with no job description at all are kept out, a rule on the defect rather than a ban on any one source.

The guard is deliberately narrow

It fires only on unambiguous markers and only for juniors. Anything ambiguous goes on to the verification pass, which has the whole profile.

# 'lead'/'leader'/'manager' are deliberately EXCLUDED - too ambiguous
# ('Lead Generation', 'Associate Manager', 'Team Lead') - and left to verification.
_SENIOR_MARKERS = (
    " senior ", " sr. ", " sr ", " staff ", " principal ", " head of ", " head, ",
    " director", " vp ", " vp, ", " vice president", " president", " chief ",
)

def is_senior_title(title: str) -> bool:
    t = f" {title.lower().strip()} "
    # 'Chief of Staff' is an early-career founder's-office role, not a C-suite seat.
    if "chief of staff" in t:
        return False
    return any(marker in t for marker in _SENIOR_MARKERS)
app/matching/seniority.py

Written down before they were built

Every real choice has a short design record: what was decided, what else was considered, and why.

pgvector, not a separate vector database

Filtered vector search is one SQL query, and there is no second store to keep in sync. Retrieval sits behind an interface so it can move if it ever needs to.

Postgres is the only source of truth

Celery and Redis move work around, but every task is idempotent and every fact lives in Postgres, so a lost message is a retry, not lost data.

Batch the nightly extraction

Pulling structured facts out of thousands of postings does not need to be instant, so it is built to run through the batch API at half the price. Curation has a deadline, so it runs live.

One channel interface

Telegram and web chat with email sit behind one messaging interface, so a new channel like WhatsApp is an adapter, not a rewrite.

One model provider

Every model call goes through OpenRouter, so switching models is a config change.

What OnePercentJobs came down to:

01

Cheapest step first

Sitemaps, then SQL rules, then vectors, then models. Each stage only sees what survived the last.

02

A model that says no

A skeptical second pass drops any pick the profile cannot back, and rewrites the reason for the rest.

03

Decide on paper

29 design records, so every trade-off was argued once and can be read again.