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#
| Guarantee | What it means | How it is enforced |
|---|---|---|
| Nothing sends without a human | Every 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 invented | No 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 well | Each 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 shown | Every 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 most | A 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 local | Secrets 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 proof | Upskill 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-apply | No 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.
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.
| Tier | Kind of action | Examples |
|---|---|---|
| 0 | Local read | Querying the database, rendering a page |
| 1 | Free external read | Fetching a careers page, a public profile, a search |
| 2 | Spend | An OpenRouter call, an Apify run — each behind a budget check |
| 3 | World-write, per item | Sending one approved email |
| 4 | Forbidden | LinkedIn login, auto-apply, bulk delete, contact export, any local model runtime |