A software company posts three openings for support agents in Leeds.
That single fact is a lead for at least three different sellers. It is a lead for whoever sells support automation, because the company has just proven it has more support volume than headcount. It is a lead for a recruiter, because they have three roles to fill. And it is a lead for whoever fits out the office those three people will sit in.
One event. Three sellers. Three completely different reads.
That observation is why Openpulse does not have a "hiring signals" listener type. It used to. And the reason it was removed is the most useful thing in this post, so we will start there.
Why "hiring signals" was the wrong abstraction
A listener type called hiring encodes exactly one reading of one situation. It says: a job posting means someone is scaling a function, and scaling a function means they need software for it. That is a fine reading. It is also one of three, and the other two could not be expressed in the product at all.
Worse, it made job postings the only situation the product could reason about in that way. A funding round, a new lease, a regulatory fine, a migration announcement and a store opening are all the same shape of evidence (a company has taken an action that implies a budgeted need), and none of them had anywhere to live.
So the abstraction moved up a level. Job boards (Ashby, Greenhouse, Lever, LinkedIn Jobs) are now ordinary sources on an ordinary market listener, and a job posting is read the same way a lease, a funding round or a public complaint is read.
The claim, in one sentence:
A situation happens, and depending on what you sell, it is an opportunity.
The classifier answers two questions, and keeps them apart
1. SITUATION What is happening, stated WITHOUT reference to the seller.
"A software company is hiring three support agents in Leeds."
2. OPPORTUNITY Does that create a need THIS seller can fill?
"They are scaling support; you sell support automation."Keeping these separate is what makes the reasoning auditable. The person reading the row sees the fact and judges the leap themselves, rather than being handed a conclusion and asked to trust it.
It has a second benefit that matters over the life of a workspace: the same situation can be re-read for a different seller later. The situation does not change. Only who it is an opportunity for.
Every other product in this space collapses these two into a single "signal detected" verdict. When that verdict is wrong you cannot tell whether the tool misread the world or misread you, which means you cannot fix it.
The rule that keeps this honest: one hop, evidence in the post
This is the constraint that separates a derived lead from an invented one.
ALLOWED "hiring 3 support agents in Leeds"
→ scaling support in Leeds [stated: role, count, place]
→ you sell support software hops = 1
REFUSED "photos from a beach party"
→ attendees are probably affluent [ASSUMED: not in the post]
→ they buy luxury cars → no opportunityTwo stacked assumptions is indistinguishable from invention. A model told to "find any way this could be a sale" will find one every single time. That is not a bug in the model, it is what you asked for.
Three mechanisms enforce the rule:
The prompt carries a worked failure, not just a worked success. A model shown only successful examples reaches further to produce one. Showing it what a refusal looks like is what makes refusal available as an answer.
Every signal records inferenceHops (0 for they-said-it, 1 for one-step-away) and the interface says plainly when a connection was inferred rather than stated. A workspace drowning in speculative leads can filter to 0 and stop guessing about which rows are which.
The prompt states which way to err, explicitly:
A wrong lead costs a salesperson a morning and some of the customer's goodwill. A missed one costs nothing they will ever notice. Prefer to miss.
That asymmetry is the correct one for outbound and it is the opposite of what volume-metered tools are incentivised to do. A tool that bills you per mention has a structural reason to prefer a false positive.
purchaseIntent measures evidence, not vocabulary
Here is a scoring bug that is worth describing in detail, because almost every intent-scoring product still has it.
The 30-point purchaseIntent component used to ask: did they say they want to buy? Which scored zero for every derived opportunity, and therefore capped every one of them at 70, below a default relevance bar of 55 only by luck, and below any sensible "call this person" threshold.
But a company that has hired three support agents has evidenced a need for support capacity far more credibly than someone idly asking Reddit for recommendations. One of them spent money. The other typed a sentence.
So the component was rewritten to measure evidence:
| Band | Meaning |
|---|---|
| 24–30 | They state the need, or are actively shopping |
| 16–23 | The need follows in one step from an action they have taken: hired, leased, raised, migrated, opened, been fined |
| 8–15 | One step from something they said but have not acted on |
| 0–7 | No need this seller can fill |
Signals scored under the new rubric are marked scoreVersion: 3. Nothing is retroactively re-scored (that is one model call per historical signal for no new information), and the version marker is what lets the interface say the scales differ rather than implying they do not.
One knock-on effect worth flagging, because it is the kind of thing that silently undoes a change like this: the label vocabulary had to move with it. The old content label listed "job ads" as an example of "nobody to talk to", so the classifier would correctly follow the one-hop rule to a real lead and then file it as marketing. And high_intent meant "explicitly asking", which capped derived opportunities at "no expressed need." A label set written against a narrower model quietly undoes a wider one.
Building it
The listener
A market listener with job boards enabled alongside the conversational sources:
| Sources | Coverage |
|---|---|
| Reddit, Hacker News | full text: the whole post, author and engagement |
| LinkedIn, X | snippet |
| Ashby, Greenhouse, Lever, LinkedIn Jobs | snippet |
| best effort |
The window is 30 days. Postings carry jobMeta (company, location, work mode, seniority, compensation where stated) alongside the usual signal fields.
The inversion that makes it work
This is the part that is genuinely hard to replicate with a keyword list, and it is the reason the input is your website rather than a search query.
Nobody advertises for an "AI receptionist."
If you sell an AI receptionist, the job title you need to watch for is "receptionist", "front desk coordinator", "patient services representative" or "scheduling coordinator": the human job your product replaces. Nobody is going to type that list into a keyword box, because working it out requires reasoning about what your product does, not about what it is called.
POST /v1/listeners/analyse-website crawls your site (landing page plus product, pricing and about pages) and derives that vocabulary. It comes back as an editable plan: keywords, exclusions, per-source queries, and the target roles. You review it before it runs. Nothing is applied silently.
Every competitor's equivalent onboarding step is an empty keyword box, and every one of them then sells you a report telling you which of your keywords are burning your quota. That is a product apologising for its own onboarding.
The accounts roll-up
Individual postings are noisy: a company posts four roles in a week and you do not want four leads. The roll-up collapses them:
GET /v1/companies?listenerId=lst_...&days=30One row per company: the count, the trend against the previous window, the ranked themes, and the strongest quote. Three support roles in a month is a different signal from one, and three this month against zero last month is a different signal again.
This is the same table as the rivals and places roll-ups: same component, same columns, different subject. The trend column is the one to sort on.
Delivery: making it land in the CRM
The whole point of a derived lead is that it arrives before the prospect starts shopping, which means it has to reach a human while it is still early. Two paths.
Webhook → your system
POST /v1/webhooks
{ "url": "https://hooks.yourco.com/openpulse", "events": ["signal.created", "run.completed"] }| Event | Fires | Carries |
|---|---|---|
signal.created | Once per signal, the first time it is seen | The signal plus its listener |
run.completed | After every signal.created for that run | Searched and kept counts |
run.failed | A run threw | The error message |
signal.urgent | A signal cleared the workspace's urgency bar | The signal.created body plus threshold, level and score |
pipeline.stage_changed | Somebody moved a deal | Entry, from-stage, who moved it, enough signal to read it |
signal.created fires on insert only. The run loop upserts, so a signal seen on an earlier run is refreshed rather than created. Firing for those would re-notify you about the same job posting every six hours, forever. If you have ever built an alert on top of a polling scraper you know exactly which bug this is.
Verification, in full:
const signed = `${req.headers["x-openpulse-timestamp"]}.${rawBody}`;
const expected = "sha256=" + crypto
.createHmac("sha256", secret).update(signed).digest("hex");
crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(req.headers["x-openpulse-signature"]),
);Sign timestamp.body, not the body alone: otherwise every delivery you ever send is replayable forever, because nothing in the signed material says when it was made. Reject anything older than five minutes. Dedupe on x-openpulse-delivery, which is stable across retries.
API key → pull on your own schedule
curl -H "Authorization: Bearer op_live_..." \
"https://openpulse.cloud/v1/results?listenerId=lst_...&classification=high_intent&limit=100"Cursor-paginated with opaque keyset markers over (sort field, id), so pages never repeat or skip a row while new documents arrive behind you.
Two properties of API keys worth knowing before you mint one:
A key is always a member, never an admin, even when an admin created it. It cannot create or run a listener (which spends money), delete a competitor, change billing, or mint another key. A "convenience" that inherited the creator's role would turn every leaked key into a full account takeover. The actor is recorded as apikey:<id> rather than as the person who created it, so a leak is distinguishable from that person's own activity in the audit log.
Scopes are deny-by-default. A route is reachable by a key only if it declares a scope, enforced by a single global hook rather than per-route middleware. That distinction is a security property, not a style preference: with per-route middleware only the routes somebody remembered to annotate are protected, and the routes they forgot answer 200 to a key scoped to nothing. Deny-by-default makes the omission fail closed.
What to do with the lead once you have it
The pipeline board takes the signal and makes it a deal. What is worth knowing is that the card denormalises what the lead was judged to be at the moment somebody decided to act on it (classification, situation category, score, urgency, title, owner) rather than joining back to the signal.
That is deliberate, and it is not just a performance choice. It is a snapshot rather than a mirror: if the signal is later re-classified, the card still says what the lead looked like when the decision was made, which explains the decision instead of quietly second-guessing it.
scoreVersion is deliberately not copied onto the card. The number is a caption there, not something to compare across rubrics.
Comments on a card are notes keyed on the signal, not on the deal. A note written while triaging on the Signals screen appears on the card, and one written on the card appears there. One lead, one conversation. The alternative, a second subject type, gives a single lead two threads on two screens and guarantees that half your team writes in the wrong one.
And the outreach draft, for the first message: three channels, three tones, written from what the posting actually says. No placeholders, no invented facts about the company, no claimed case studies, and none of the openers that announce themselves as generated. There is no send button and there will not be one: the moment outreach can go out unread, signal quality stops mattering.
The honest limits
Job boards are a snippet source. LinkedIn Jobs, Ashby, Greenhouse and Lever come back through a search index, so the classifier is judging a title, a company and a snippet rather than the full posting body. Reddit and Hacker News are full text; job boards are not. That is labelled per source in the wizard rather than left for you to discover from a run of bad labels.
Derived leads are earlier and colder than stated ones. A purchaseIntent of 18 means the need follows in one step from an action they took, not that they are shopping. The right play is a relevant, specific first message, not a demo request.
One hop, not two. If your product's connection to the situation genuinely requires two inferences, this pipeline will refuse it and it is right to refuse it. That is a real constraint on which businesses this works for, and it is better to know that in week one.
Nothing is real-time. Cadence is hourly at the fastest, on Pro. A job posting is a signal with a shelf life of weeks, so this is not the axis that matters here, but if you need sub-minute, this is not that product.
The setup, condensed
- Create a
marketlistener from your website. Enable the job boards alongside Reddit and Hacker News. - Read the derived keywords carefully. The test is whether it named the human role your product replaces, not your product category. Fix it if it did not: this is the highest-leverage edit you will make.
- Set
minRelevanceScoreat the default 55 for the first two weeks and watch what lands. Do not raise it until you have thumbs-downed a few: labelled examples calibrate the next pass, and a query that produces candidates but no signals for three runs disables itself and says so in the run log. - Filter to
inferenceHops: 0if the derived leads feel too speculative for your team. Turn it back on when they are ready for them. - Read the accounts roll-up weekly, sorted by trend. Work companies, not postings.
See it on your own market
Paste your website, review the plan it proposes, and read what comes back tomorrow morning.
Questions about anything here? Email support@openpulse.cloud.