1.8 million posts.
53 seconds. 65 cents.
Two jobs every sales team does by hand: find the people who are ready to buy, then make sure the message you send each one is actually written for them. Here are both as scripts, the three questions that do the filtering, and what happened when I ran them.
A quick mental model
Jev takes a situation and answers small typed questions about it, with a probability on each. Both jobs are the same move with a different situation:
- Finding buyers: the state is one post. The question is whether its author wants what you sell.
- Checking messages: the state is one person plus the message you’re about to send them. The question is whether it lands.
New to Jev? Start with the setup guide — key, first call, working response. Then come back.
Search where buyers ask, not where they talk
This matters more than anything the model does. I pulled the 40 most recent Hacker News comments containing “cold email” and scored every one: zero buyers. The top score was 0.12. They were people arguing about AI, reply rates, and email clients.
Then I searched Ask HN posts about CRMs — people asking a room which tool to pick. Same questions, same model: 6 of 30 came back as buyers, every one of them someone choosing a CRM for their team.
One call per post, three questions
Every question is judged in parallel against the same post in a single call, so asking three costs about what asking one does. These three are the whole filter:
- buying — “The author is actively looking for what you sell right now, and would welcome a reply offering one.”
- pain — “The author describes a problem they personally have today.”
- vendor — “The author is promoting or selling their own product or service.”
Keep a post when buying clears 0.60 and vendor is under 0.50. Pain is your sort order when you have more leads than time.
The vendor question is the one people skip
The people who talk most about your problem are the ones selling a fix for it. Without a vendor filter, every “we built a tool for this” post floods the top of your list. In my runs the vendor question caught a job ad, a hiring post and a product launch that all mentioned the search term.
It also correctly passed on people building a CRM and people wanting a personal CRM — mentioning the thing is not the same as wanting to buy it from you.
Check every message before it goes
This is the second demo: 700 leads, each with the personalised message about to be sent, scored in 40 seconds for 9 cents. The state is the person and the message together, and you ask two things: would they reply, and was this written for someone like them.
I tested it on three pairs. The first two share a message word for word, and only the person changes:
0.66 0.90 send Head of Sales, SDRs drowning in account research
0.17 0.08 WRONG PERSON Backend engineer, same message
0.19 0.32 WRONG PERSON VP Marketing, "Hi {first_name}, I help companies grow"The same sentence goes from 0.90 to 0.08 on fit when it’s aimed at someone it wasn’t written for. That’s the “right message, wrong person” catch, and it’s the mistake a mail-merge makes silently at scale.
What to do with the scores
- Fit under 0.50 — don’t send. The message isn’t for this person; rewrite it or drop them.
- Fit fine, reply under 0.50 — right person, weak message. Rewrite and score it again; it costs a fraction of a cent.
- Both clear — send it yourself.
Generic templates score low on fit for everyone, which is the point: “I help companies like yours grow” isn’t written for anyone.
Let an agent build it
Paste this at Claude Code or Codex. It carries the three questions, both thresholds, the rate-limit fix, and the guardrail that keeps it from sending anything on your behalf.
Build me a two-part prospecting job using Jev through the Vercel AI Gateway.
PART 1 — FIND BUYERS
1. Ask me what I sell, in one plain sentence, and where my buyers ask for it (a subreddit, an X search, Ask HN).
2. Pull the last 30 days of posts from there. Search where people ASK for a thing, not where they mention it.
3. For every post make ONE call: experimental_evaluate from the ai package, model typesafe-ai/jev, the post as the state, and three boolean questions together:
- buying: "The author is actively looking for <what I sell> right now, and would welcome a reply offering one."
- pain: "The author describes a problem they personally have today."
- vendor: "The author is promoting or selling their own product or service."
4. Keep a post only if buying >= 0.60 AND vendor < 0.50. People selling the same thing mention the same words.
5. Run 3 calls at a time, not hundreds; bursts get rate-limited. Write leads.csv (url, buying, pain, vendor).
PART 2 — CHECK MESSAGES BEFORE THEY GO
6. For each lead I approve, I will write one line on who they are and the message I want to send.
7. One call per lead: the state is "LEAD: <who they are> / MESSAGE THEY ARE ABOUT TO RECEIVE: <message>", with two boolean questions: "This person would reply to this message." and "This message was written for someone in this person's role, with this person's problem."
8. Fit under 0.50 means the right message is going to the wrong person: flag it, do not send it.
Stop after part 1 and show me the CSV. Never send, post or DM anything yourself — I review every lead and send by hand.Or run it yourself: find buyers
Set what you sell and where to look, get a CSV of posts worth replying to. Runs on the free Hacker News search API; swap the fetch for Reddit or X once it works.
// npm install ai
// .env: AI_GATEWAY_API_KEY=...
// run: node --env-file=.env find-buyers.mjs
import { experimental_evaluate as evaluate } from 'ai';
import { writeFileSync } from 'node:fs';
const PRODUCT = 'a CRM for a small sales team'; // what you sell, in plain words
const SEARCH = 'CRM'; // where buyers ask, not what they mention
const SINCE_DAYS = 3650; // tighten to 30 for live prospecting
const KEEP = 0.60; // buying score to keep
const since = Math.floor(Date.now() / 1000) - SINCE_DAYS * 86400;
const res = await fetch('https://hn.algolia.com/api/v1/search?tags=ask_hn&hitsPerPage=30' +
'&numericFilters=created_at_i>' + since + '&query=' + encodeURIComponent(SEARCH));
const posts = (await res.json()).hits.map((h) => ({
url: 'https://news.ycombinator.com/item?id=' + h.objectID,
text: (h.title + ' -- ' + (h.story_text ?? '')).replace(/<[^>]+>/g, ' ').slice(0, 6000),
}));
const questions = { // ONE call per post, all three questions
buying: { type: 'boolean', instructions: 'The author is actively looking for ' + PRODUCT +
' right now, and would welcome a reply offering one.' },
pain: { type: 'boolean', instructions: 'The author describes a problem they personally have today.' },
vendor: { type: 'boolean', instructions: 'The author is promoting or selling their own product or service.' },
};
async function pool(items, n, fn) { // a few at a time: bursts get rate-limited
const out = []; let next = 0;
await Promise.all(Array.from({ length: n }, async () => {
while (next < items.length) { const i = next++; out[i] = await fn(items[i]); }
}));
return out;
}
const scored = await pool(posts, 3, async (p) => {
const { answers } = await evaluate({ model: 'typesafe-ai/jev', state: p.text, questions, maxRetries: 3 });
return { ...p, buy: answers.buying.probability, pain: answers.pain.probability, vendor: answers.vendor.probability };
});
const leads = scored
.filter((p) => p.buy >= KEEP && p.vendor < 0.5) // sellers mention the topic too — drop them
.sort((a, b) => b.buy - a.buy);
writeFileSync('leads.csv', ['url,buying,pain,vendor',
...leads.map((p) => [p.url, p.buy.toFixed(2), p.pain.toFixed(2), p.vendor.toFixed(2)].join(','))].join('\n'));
console.log(leads.length, 'of', posts.length, 'posts are buyers -> leads.csv');And check the messages
Put one line per lead and the message you plan to send in outreach.json, and it tells you which to send, rewrite, or skip.
// npm install ai
// .env: AI_GATEWAY_API_KEY=...
// run: node --env-file=.env check-messages.mjs
// outreach.json: [{ "lead": "who they are, in a sentence", "message": "what you are about to send" }, ...]
import { experimental_evaluate as evaluate } from 'ai';
import { readFileSync } from 'node:fs';
const rows = JSON.parse(readFileSync('outreach.json', 'utf8'));
const questions = {
reply: { type: 'boolean', instructions: 'This person would reply to this message.' },
fit: { type: 'boolean', instructions: "This message was written for someone in this person's role, with this person's problem." },
};
for (const r of rows) {
const { answers } = await evaluate({
model: 'typesafe-ai/jev',
state: 'LEAD: ' + r.lead + '\nMESSAGE THEY ARE ABOUT TO RECEIVE: ' + r.message,
questions,
maxRetries: 3,
});
const fit = answers.fit.probability, reply = answers.reply.probability;
const verdict = fit < 0.5 ? 'WRONG PERSON — rewrite or skip' : reply < 0.5 ? 'weak — rewrite' : 'send';
console.log(reply.toFixed(2), fit.toFixed(2), verdict, ' ', r.lead.slice(0, 60));
}The honest part
The headline numbers are other people’s runs. 1,759,932 posts in 53 seconds for 65 cents is @tarasshyn’s buying-signal demo. 700 leads and messages in 40 seconds for 9 cents is @romanbuildsaas’s. Both posted 18 September 2026. They’re separate projects; this page puts them side by side.
My runs were small and slow. Dozens of posts, not millions, and when I fired 40 calls at once the gateway came back with “the upstream provider is currently experiencing high demand.” Three at a time finished cleanly. That’s why the scripts use a small pool — if you scale up, raise it gradually and keep the retries.
The message test is three hand-written pairs, a sanity check rather than a benchmark. The pattern held (the mismatch dropped to 0.08), but score your own real outreach against your own reply data before you trust a threshold.
Nothing here sends anything. It hands you a list. Read the post before you reply, answer the question they actually asked, and follow each platform’s rules — a buying signal is an invitation to be useful, not a license to spam.
Don’t have Jev running yet?
The setup guide is one key and a few lines of code — a working response in a few minutes.
Get started with Jev →