Skip to content
Concepts

Guarantees & guardrails

The review gate, the send cap, evidence on every field — what is never cut.

A tool that writes to strangers from your own inbox has to be trustworthy before it is clever. These are the promises ZoNuLy keeps no matter what — and the code that keeps them.

The guarantees#

GuaranteeWhat it meansHow it is enforced
Nothing sends without a humanEvery email is approved by you, as written, before it can leave.The sender refuses any email that isn't approved; approval is set only by the approve route; no scheduled job approves.
Nothing is inventedNo project, number, title or connection that isn’t in the source material.The composer leaves a slot empty rather than guess — and an empty required slot means no email. Review flags mark unsupported claims.
25 a day, spent wellEach send is irreversible and burns reputation; the daily cap is a budget.A send ledger is checked inside the send itself; the cap is visible on every screen.
Confidence is always shownEvery address is verified, pattern-guessed or scraped.The label is never defaulted, merged or hidden; guessed addresses have their own daily cap and a bounce brake.
One follow-up, at mostA silent thread gets one polite nudge after five days — never two.A per-thread flag is set when the follow-up is queued, and the follow-up goes back through review.
Data stays localSecrets never leave the laptop; every outbound call is logged, priced and capped.Keys live in .env (git-ignored). The resume and drafts are sent to OpenRouter models, by design.
Nothing added to your profile without proofUpskill never puts a skill on your profile that you haven’t shown.“Done” needs a proof link and a line in your own words; only then can it be added, after a dated backup of the profile.
No auto-applyNo portal applications, no contact-list exports. One person, one role, one ask.There is no code path that submits an application.

The guardrails layer#

Every value a scraper finds passes through jobhunter/guardrails.pybefore it is written. Guardrails don't just validate shapes — they decide whether a fact is believable and record why.

Descriptions
Must be 60–600 characters, about this company (its name or domain), not a cookie banner, bot wall or generic tagline. Refusals are counted by reason on the Scraping health page.
Funding amounts
Between $0.05M and $5,000M; anything else is a misparse.
Contacts
vet_contact checks the name is a person, the address is on the company’s domain and not a shared inbox, and attaches an affiliation with its evidence.
Hiring
Proof must come from the company’s own careers page or ATS board, and goes stale after 30 days.
Field evidence
Each field carries its source, channel and confidence in field_meta — the dashboard shows the sentence a number came from.
Complete bar
A company counts as complete only with a website, a real description, hiring proof and a lead with an address.

Hard constraints#

No local models#

The laptop has 8 GB of RAM and running a local model wrecked it, so every model call goes to OpenRouter behind a ledger with per-minute, daily and monthly caps. Nothing in the repo starts a model runtime.

No personal accounts in scraping#

Your LinkedIn, X and GitHub logins are never used to scrape. LinkedIn is read only through public guest pages, paced, with a circuit breaker that stops for hours on the first block. Where a logged-in view is genuinely needed (a company's People tab), it is bought from Apify, whose own accounts carry the risk.

Never send like spam#

  • 25 emails a day, spread across 10:00–19:00 — about one every twenty minutes, jittered.
  • At most one email per run of the send job, so a laptop waking from sleep never fires a backlog.
  • Written the way Gmail's own compose writes: text with a minimal HTML twin, links on words. No tracking pixels, no link shorteners, no templates with styles or images, no attachment.
  • At most three cold emails a day into one company's domain, and a review flag on any near-copy of mail already going there.
  • A bounce brake: past six bounces in seven days, guessed addresses stop; verified ones still go.
  • After Gmail refuses any mail as spam: no cold mail for 48 hours, and none into that company for 21 days.
Learned the hard way
On 19 Oct 2026 about thirty near-identical emails went into one company a minute or two apart. Four guessed addresses bounced and Gmail refused all further mail for a day (550 5.4.5). The pacing rules above exist because of that day. On 23 Oct a different signal arrived — 9 of 24 asks into one company refused as spam (5.7.1) — and the per-domain rules above came from that one. See Troubleshooting.

Permission tiers#

Every action the system can take belongs to a tier. The higher the tier, the more it needs your explicit grant.

TierKind of actionExamples
0Local readQuerying the database, rendering a page
1Free external readFetching a careers page, a public profile, a search
2SpendAn OpenRouter call, an Apify run — each behind a budget check
3World-write, per itemSending one approved email
4ForbiddenLinkedIn login, auto-apply, bulk delete, contact export, any local model runtime
Nothing is built without a numbered approval
Changes to the pipeline are proposed, numbered, and built only once approved. Agents propose; humans commit.