listen · detectors
Match rules and vocabularies
How a rule decides a post matches.
In Detectors
Match rules and vocabularies: how a rule decides a post is the trigger.
A match rule is the logic inside a detector. Cheap deterministic clauses (phrases, negatives, fields) narrow the stream; a judge reads the survivors; strength, freshness and exclusions decide whether a signal is emitted. Twelve worked rules below, one per common pattern.
Triggers → Detector catalog → Match rules → Signal catalog
Cheap first, model last — Phrases and fields are free; the judge costs money and runs only on survivors.
Buyer's words only — Vocabulary comes from what buyers actually write.
Every no has a reason — Each clause that fails is logged; those are the negatives.
what it is
A boolean query, a lexicon and a judge.
Industry names: boolean queries and keyword taxonomies in social listening, rule-based filters in intent data, LLM-as-judge for the final call. A good rule is high recall at the cheap layers and high precision at the judge.
layer 1 — Deterministic clauses · any_of, all_of, NEAR, none_of, fields, author filters. Fast, free, explainable.
layer 2 — The judge · One question, a rubric, a minimum confidence. Reads context the phrases can't.
layer 3 — Score and gate · Strength, freshness on the item's own date, exclusions, dedupe. Then emit.
how a rule runs
Every item takes the same path.
- Item — post · record
- Fill — placeholders
- Phrases — any_of · all_of
- Negatives — none_of
- Fields — author · numbers
- Judge — rubric, conf.
- Score — strength · fresh
- Exclusions — not-if · lead rules
- Emit — signal + reasons cheap and deterministic first; the model only sees survivors every clause that fails is logged with its reason (a discard)
order matters for cost
Phrases, negatives and field checks are free. The judge is the only step that costs money, so it runs last and only on survivors. Log which step stopped each item: those discards are the judge's training negatives.
the rule object
A rule, written down.
The sample is the X rule for flopped launches, version 3, after two rounds of negatives from real false matches.
| Field | Type | What it holds |
|---|---|---|
any_of | phrase list | At least one must appear. Supports {placeholders} and NEAR(n) for words within n words. |
all_of | phrase list | Every group must appear. |
none_of | phrase list | Any hit throws the item out. Grows from real false matches. |
fields | conditions | For threshold and event rules: field, operator, value (upvotes < 10, rating <= 3). |
author | filters | Account age, bio has a product link, not in lead_rules.never, not a known vendor. |
within | duration | Freshness on the item's own date (posted_at or event_date). |
requires | rule | Stacking: only emit when a second signal exists for the same person. |
judge | object | prompt_version, model, the question, a rubric, min_confidence. Optional for owned rules. |
strength | enum | strong, medium or weak, with rules for upgrades (e.g. paid slot). |
exclude | rule list | Detector-specific plus the ICP's not-if lines and the lead rules, always. |
dedupe | key | How repeats collapse: url, or company + title for job posts. |
version | integer | Bumped on any change and stamped on every match. |
one rule, as data
{
"id": "det.tried-it-and-it-failed.x",
"version": 3,
"kind": "phrase",
"source": "x",
"match": {
"all_of": [
[
"launched",
"shipped",
"went live"
]
],
"any_of": [
"crickets",
"0 signups",
"zero signups",
"no sales",
"nobody signed up"
],
"none_of": [
"lol jk",
"#1 product of the day",
"thank you all"
],
"within": "7d"
},
"author": {
"exclude_lists": [
"lead_rules.never",
"known_vendors"
],
"bio_has_link": true
},
"judge": {
"prompt_version": "flopped_launch.v1",
"question": "Did the author launch their own product recently and get little or no response?",
"rubric": [
"own product",
"launched within 30 days",
"explicitly low response"
],
"min_confidence": 0.7
},
"strength": {
"default": "strong",
"downgrade_if": "no numbers and no second signal"
},
"exclude": [
"icp.not_if",
"lead_rules.never",
"lead_rules.competitors"
],
"dedupe": "url",
"log": {
"on_discard": [
"clause",
"value"
],
"on_emit": [
"clause_hits",
"judge_verdict",
"confidence"
]
}
}vocabularies
Where the words come from.
The vocabulary is the most valuable part of a rule and the easiest to get wrong. Each type has a source; none of them is the founder's own marketing copy.
| Type | What it is | Example | Source |
|---|---|---|---|
| ask words | phrases people use when choosing a tool | "what do you use for", "any tool that", "recommend" | audience research, the trigger |
| pain words | the problem in the buyer's own words | "launched, crickets", "0 signups", "nobody knows it exists" | Reddit and X threads, founder interview |
| job and category | what the product does, what buyers call it | {job} = "get my first customers"; {category} = "distribution tool" | the brief |
| competitors | names, old names, misspellings | {competitor} from the competitor profiles | market research |
| ICP markers | words that show fit | "solo founder", "my SaaS", "bootstrapped", a product link in the bio | ICP fit lines |
| negatives | words that show a false match | "we built", "affiliate", "top 10", "#1 product of the day" | every false match you've read |
| roles | the person who does the job by hand | {role} = "social media manager" | ICP, job boards |
the catalog
Twelve worked rules.
One per common pattern: asks, flops, thresholds, research-only reviews, change-arms-phrase, events with dedupe, weak stacking rules, and owned conditions.
phrase — det.asking-for-a-recommendation.reddit
Asking for a recommendation · Reddit
any_of: "what do you use for {job}" | "best tool for {job}" | "any recommendations" NEAR(8) "{category}"
none_of: "we built" | "I made" | "our tool" | "affiliate" | "top 10"
author: not in lead_rules.never; not a known vendor
within: posted_at > now - 7d
judge: rec_request.v1 (min_confidence 0.7)
strength: strong- judge asks
- Is the author asking which tool to use for {job} for themselves, right now? Answer no if they are a vendor, if the thread is a listicle, or if they already chose.
- why
- The NEAR clause stops 'any recommendations' matching every question in the subreddit.
phrase — det.tried-it-and-it-failed.x
Tried it and it failed · X
all_of: "launched" | "shipped" | "went live"
any_of: "crickets" | "0 signups" | "zero signups" | "no sales" | "nobody signed up"
none_of: "lol jk" | "finally" | "#1 product of the day" | "thank you all"
within: posted_at > now - 7d
judge: flopped_launch.v1 (min_confidence 0.7)
strength: strong- judge asks
- Did the author launch their own product recently and get little or no response? Answer no for humble-brags, retrospectives older than a month, or someone else's launch.
- why
- The humble-brag negatives were added from real false matches; that's how none_of lists grow.
threshold — det.tried-it-and-it-failed.uneed
Tried it and it failed · Uneed
record: uneed.listing
where: upvotes < 10 AND launched_at > now - 7d
boost: spotlight = true (paid slot, still low votes)
owner: listing.maker OR listing.domain
exclude: category in (agency, template, non-software)
strength: strong- judge asks
- Is this a software product sold by a small team (not an agency, template or directory)?
- why
- A paid Spotlight slot with single-digit upvotes is a stronger signal than a free listing.
threshold — det.set-up-but-no-results.mrr
Set up, but no results · revenue board
record: revenue_board.startup
where: customers > 0 AND mrr = 0
OR mrr flat for >= 3 months
exclude: free products listed for the badge
strength: strong · fresh_for 30d- judge asks
- Does this startup sell a paid product with real customers but no revenue growth?
- why
- Public numbers let the first message be specific without being creepy: they published them.
threshold — det.unhappy-with-their-current-tool.g2
Unhappy with their current tool · G2
record: g2.review where product = {competitor}
where: rating <= 3 AND published_at > now - 30d
extract: complaint, role, company_size
route: research (no reachable person) -> comparison pages
strength: strong for content, weak for outreach- judge asks
- Name the single biggest complaint in this review, in the reviewer's words.
- why
- A detector can be right and still be research-only. Its output feeds comparison pages, not DMs.
phrase — det.unhappy-with-their-current-tool.reddit
Unhappy with their current tool · Reddit
any_of: "cancelling {competitor}" | "switching from {competitor}" | "{competitor} is too expensive" | "alternative to {competitor}"
none_of: "{competitor} team here" | "I work at"
within: 7d
judge: competitor_dissat.v1
strength: strong- judge asks
- Is the author a current or recent paying user of {competitor} who wants out?
- why
- Competitor names come from the competitor research; misspellings and old names belong in any_of too.
change — det.competitor-shake-up.comp
Competitor shake-up · pricing page
watch: {competitor}/pricing, /changelog, status page
diff: prices, plan names, "end of life", "sunset", "discontinued"
ignore: layout, images, scripts
then: arm det.unhappy-with-their-current-tool.* on {competitor} for 7d
strength: strong (event)- judge asks
- Did a price, plan or availability change? Summarize it in one line.
- why
- A change detector arms phrase detectors: the angry threads come a day or two later.
event — det.hiring-for-the-job-you-do.jobs
Hiring for the job you do · job boards
record: job_post where title ~ {role}
where: company_size in icp.size AND posted_at > now - 30d
exclude: agencies hiring for clients; companies over icp.size
dedupe: company_domain + normalized_title
strength: strong- judge asks
- Is this company hiring a person to do the job {product} does, at a size we sell to?
- why
- Dedupe on company and title: the same post appears on three boards.
phrase — det.doing-it-by-hand.reddit
Doing it by hand · Reddit
any_of: "template for {job}" | ("spreadsheet" | "google sheet") NEAR(10) "{job}" | "I built a script"
none_of: "selling" | "my template" | "gumroad"
within: 14d
judge: manual_workaround.v1
strength: strong- judge asks
- Is the author doing {job} and unhappy about the time it takes?
- why
- Template sellers look identical to template seekers until the judge reads the post.
phrase — det.venting-about-the-problem.x
Venting about the problem · X
any_of: pain_words(lane) # from audience research, buyer's own words
within: 7d
requires: second_signal(same person, 14d)
strength: weak- judge asks
- Is the author personally feeling {pain}? (not a joke, a quote or a general take)
- why
- Weak alone. The requires clause turns it into a stacking rule instead of a lead source.
your data — det.hand-raise.replies
Hand-raise · replies
event: reply on post where post.play = comment-a-keyword
where: reply contains URL OR reply ~ "{keyword}"
exclude: bots; people after exposure, not help
emit: person, url, reply_text
strength: strong · fresh_for 2d- judge asks
- Did they ask for the offer and give what it needs (a product link)?
- why
- Deliver privately and fast; this is the only cold-to-warm handoff the person started.
your data — det.trial-ending-not-converted.stripe
Trial ending, not converted · Stripe
event: customer.subscription.trial_will_end
where: usage(last 7d) < activation_threshold AND no payment method
exclude: internal accounts
strength: strong · fresh_for 3d- judge asks
- (no judge: a rule on your own events)
- why
- Stripe sends this event 3 days before a trial ends; the usage condition decides whether to act.
failure modes
How rules go wrong.
| Failure | What it looks like | How to catch it |
|---|---|---|
| Too broad | Hundreds of matches a day, most of them noise. | Add NEAR clauses and negatives before adding judge calls; read 20 and count. |
| Too narrow | A trigger you know is happening never fires. | Log people you found another way; each miss becomes a new any_of phrase. |
| Founder's words, not the buyer's | Phrases like 'distribution agent' that buyers never type. | Vocabulary only from audience research and real posts. |
| Negation and sarcasm | "not looking for a scheduler, lol" matches 'scheduler'. | The judge reads the whole post; phrase rules alone can't. |
| Unfilled placeholders | {competitor} left empty matches the literal text, or everything. | Rules refuse to run until the brief fills every placeholder. |
| Judge drift | A prompt edit quietly changes what counts. | prompt_version on every verdict; replay the labeled set before shipping a new version. |
| Unversioned edits | Precision changes and nobody knows why. | Every rule change bumps version; matches carry it. |
data to capture
Log the clause, not just the verdict.
So a new rule version can be replayed on stored items before it ships.
record
- rule version and judge prompt_version on every match
- which clauses hit, with the matched text
- every discard with the clause that stopped it
- judge verdict, reason and confidence
label
- founder real or not on a daily sample
- judge vs founder disagreements, kept as a set
- misses: people found another way the rule didn't catch
trains
- the match judge (distilled to a small model)
- learned negatives: phrases that predict a false match
- vocabulary expansion from posts that converted
test
- rule v2 vs v1 by offline replay on stored items
- judge prompt v2 on the labeled set before shadow
- NEAR(8) vs NEAR(15) on recall and precision