SignalPipe
Buying-intent judgement for AI agents: three judges decide whether the author of any post or message is a real buyer, and draft a reply you approve.
What it can do
- Get Missions: List pending lead missions awaiting review. Each mission includes: id, score, channel, lead snippet, prospect handle, role (closer/advisor/educator), and the drafted reply if any. Presen
- Draft Mission: Get the drafting payload for a single mission so you can write the reply. This is a working payload, not a display payload — it intentionally contains the product positioning, lead text
- Upload Draft: Upload a reply draft you wrote for a mission. The mission moves to pending_approval and surfaces for the operator to approve or edit. `reasoning` is optional, one-sentence justification
What data it sees
Do you need an account
No: the server works without sign-in
Buying-intent judgement for AI agents: three judges decide whether the author of any post or message is a real buyer, and draft a reply you approve.
- Scores any text your agent reads, from anywhere
- An optional scout reads the RSS or Atom feeds you choose
- When the judges split on a lead, you see it
- Only real leads reach your queue, each with a drafted reply you approve
Needs a paid SignalPipe plan and your operator key. Details: https://signalpipe.io
How to connect
How to connect SignalPipe to Claude
The server address to paste into Claude settings:
https://signalpipe--signalpipe.run.tools
- Open Claude (claude.ai or the desktop app).
- Go to Settings → Connectors.
- Click Add custom connector.
- Paste the server address copied below into Remote MCP server URL and click Add.
- In a chat, click + → Connectors and switch the new connector on.
Custom connectors are available on Free, Pro, Max, Team and Enterprise plans (Free is limited to one). On Team and Enterprise an organization Owner adds the connector first under Organization settings → Connectors.
Authorization
After you click Add, a sign-in window for the service opens. Sign in with your own account and approve access. Claude never sees your password.
How to connect SignalPipe to ChatGPT
The server address to paste into ChatGPT settings:
https://signalpipe--signalpipe.run.tools
- Open ChatGPT in a browser (chatgpt.com). A Plus, Pro, Business, Enterprise or Edu plan is required.
- Turn on developer mode once: Settings → Apps → Advanced settings → Developer mode.
- Open Settings → Connectors and click Create.
- Fill in the form: Name (anything), Description (one line about what the service does), MCP server URL (copy it below).
- Under Authentication choose OAuth if the service requires sign-in, otherwise None. Click Create.
- In a new chat open + → Apps/Connectors and enable the connector.
OpenAI has renamed this section before (Connectors → Apps/Plugins). If the label differs, search settings for "developer mode". On Business/Enterprise workspaces an admin must allow custom connectors first.
Authorization
On first use ChatGPT opens the service's sign-in window. Sign in and approve access.
How to connect SignalPipe to Cursor
The server address to paste into Cursor settings:
https://signalpipe--signalpipe.run.tools
Fastest: click Open in Cursor below and confirm the prompt.
Manually:
- In Cursor open Settings → Cursor Settings → MCP and click Add new global MCP server.
- Paste the JSON copied below into
~/.cursor/mcp.json(per project:.cursor/mcp.jsonin the repo root). - Save the file. The server appears in the MCP list; authorize it there if asked.
Authorization
If the service needs sign-in, an authorize button appears next to the server in the MCP list.
Server tool list (24)
Raw names from tools/list. Only developers need these.
| get_missions | List pending lead missions awaiting review. Each mission includes: id, score, channel, lead snippet, prospect handle, role (closer/advisor/educator), and the drafted reply if any. Presentation: format the response as a numbered list inline. For each mission show score, role, handle, snippet, and draft. Do NOT run shell commands or write files to inspect this response — the data is already structured. Read each mission directly from the JSON. |
| draft_mission | Get the drafting payload for a single mission so you can write the reply. This is a working payload, not a display payload — it intentionally contains the product positioning, lead text, scoring breakdown, and response schema your draft must follow. Use it to write the draft; do NOT dump the full payload back to the user. Workflow: 1. Call this with the mission_id. 2. Compose a reply that fits the role/tone in `context.strategy`. 3. Call `upload_draft(mission_id, draft, reasoning)` with your draft. 4. If the lead is clearly not a buying signal, skip drafting and call `reject_mission(mission_id, rejection_reason)` instead. |
| upload_draft | Upload a reply draft you wrote for a mission. The mission moves to pending_approval and surfaces for the operator to approve or edit. `reasoning` is optional, one-sentence justification for the audit log. |
| approve_mission | Approve a mission and queue it for outreach. Teaches the RL loop. Approval is the operator's signal that this lead is worth pursuing. Today (manual outreach phase) it doubles as the "I have replied or am about to" confirmation — so approve also rewards the source feed, treating the mission as a positive outcome (`replied`). When the sidecar is wired for autonomous Reddit/Twitter sending, outcome tracking will move to a separate feedback step and approval will only mean "queue for outreach". Pass `edited_draft` if the operator edited the wording — otherwise the existing draft is used unchanged. |
| reject_mission | Reject a mission — it was not a real buying signal. Use this when you want the system to LEARN from the rejection. Each reason maps to a different RL penalty applied to the source feed's scoring weight, so the swarm gets smarter about that product's leads over time. When to use this vs `delete_mission`: - `reject_mission`: the lead was a bad signal — feed it back so the scoring weight adjusts. Always prefer this when you have any opinion on why the lead was wrong. - `delete_mission`: queue cleanup only (duplicates, stale leads, things you don't want to teach the system about). NO learning. DON'T use `reject_mission` when the post is gone (deleted by the poster, 404'd, removed by mods, or stale beyond your reply window). The lead wasn't bad — it just disappeared. Use `delete_mission` instead, which leaves the source station's `rl_weight` untouched. Reaching for `reject_mission(not_relevant)` here penalises a station that did nothing wrong. Valid `rejection_reason` values: - `spam` — bot/promoted/automated post (heaviest penalty) - `not_relevant` — wrong audience or topic - `wrong_product` — relevant signal but wrong product matched it - `too_vague` — signal too weak to act on - `sarcasm` — venting/ironic, not a real buyer - `already_customer` — they already bought (no penalty) - `no_reason` — default if you don't have a strong opinion Pick the most accurate reason — they apply different penalty sizes, so accuracy directly improves how the system learns. |
| delete_mission | Hard-delete a mission from the queue — silent cleanup only. Use this only when you want to clear the row without teaching the system anything: duplicates, accidental scrapes, leads the operator doesn't want surfaced again but doesn't have an opinion on. Canonical case: the post is gone by the time you get there — deleted by the poster, 404'd, removed by mods, or stale beyond your reply window. The lead wasn't wrong, the opportunity just evaporated. `delete_mission` is the right tool because there's nothing to learn from the station — its `rl_weight` stays untouched. Using `reject_mission` here would unfairly penalise a station for an event it didn't cause. When to use this vs `reject_mission`: - `delete_mission`: queue cleanup, no learning. The scoring weight for the source feed is untouched. Canonical uses: post deleted, duplicates, accidental scrapes. - `reject_mission`: the lead was a bad signal and you want the system to learn from it. Always prefer this when you have any opinion on WHY the lead was wrong — the RL loop can only sharpen if you give it the rejection reason. Default: if a lead looks like noise but you can categorise WHY (spam, not relevant, too vague, etc.), reach for `reject_mission` first. |
| mark_sent | Mark a mission as sent by hand — "I already messaged this person myself." Use this when YOU sent the outreach manually (via the Reddit/X UI, a public comment, or a DM) instead of letting an autonomous sender do it. Many leads are surfaced on the `manual` channel precisely so the operator reviews and sends them personally — and first contact is often a public reply rather than a DM. Without telling the system the send happened, the mission sits unfinished, no prospect is created, and a later follow-up draft has no record of what you already said. What this does: - marks the mission as sent, - records the outreach so follow-up drafts know the conversation history (creates/updates the prospect and starts its lifecycle), - counts as a positive endorsement for the source feed (same learning signal as `approve_mission`). When to use this vs the others: - `mark_sent`: you sent it yourself, by hand. Records the send + starts the prospect. Use this the moment you've actually messaged or replied to the person. - `approve_mission`: queue it for an autonomous sender to deliver (you are NOT sending it yourself right now). - `reject_mission` / `delete_mission`: you are NOT reaching out. Idempotent: if you already called `approve_mission` on this lead, calling `mark_sent` afterwards will not double-count anything — it just records the actual send. |
| scout_now | Trigger an on-demand scouting run across YOUR active products. Scouts run automatically every 30 minutes. Use this when the operator wants a fresh scan now. At most one on-demand scan per tenant every 15 minutes; returns status "skipped" with a reason (cooldown / batch_in_progress) when a scan is not started. |
| score_signal | Score arbitrary text for buying-intent / competitor-mention / churn / noise. Use this when your agent encounters content from any channel it has access to — email, Slack, Discord, Telegram, LinkedIn, WhatsApp, a YouTube comment, a blog post — and you want to know whether it's a real signal worth acting on, before drafting a reply. SignalPipe doesn't connect to the source channel. Your agent reads the text (via whatever plugin gives it that access) and passes the text here. We run the same scoring engine the scout uses on Reddit and HN: keyword gate, multilingual semantic scoring (EN/ES/FR/DE/PT/FI), 3-judge swarm pattern, sarcasm detection, and competitor-intent classification. Returns: score (0-100), role (closer/advisor/educator), classification (buying_intent / competitor_mention / borderline / noise), sub-scores (urgency, specificity, keyword_density), competitor info if matched, and a `drafting_context` block when the score is high enough to act on. When the borderline band triggers a panel review you also get `swarm_ran: true` and a `swarm` block: each judge's stance ("convinced" / "on the fence" / "unconvinced") and `split: true` when they disagreed materially. A split panel is worth flagging to the operator — it usually means the post is genuinely ambiguous rather than clearly a lead or clearly noise. You write the reply yourself using the context — same pattern as `draft_mission`, no server-side LLM call required. ⚠️ CHECK `degraded` BEFORE YOU TRUST A LOW SCORE. There is a per-tenant daily budget for panel reviews. `degraded: true` means this text WAS borderline but the panel did not get to judge it — either the budget ran out (`swarm_skipped: "daily_cap_reached"`) or the panel errored (`"panel_failed"`). The score you are holding is the content-only score, which is exactly the score the panel exists to correct: it under-reads real buyer |
| get_pipeline | List the prospect pipeline sorted hottest-first. Each prospect includes: id, handle, channel, temperature (0–100), mode, last_signal, last_contact, and any recorded objections. Modes (intent-based, not pure temperature): - nurture — first-touch / no engagement yet. Educator persona, value-first messaging. Default for newly tracked prospects, regardless of starting temperature. - sales — prospect has engaged (replied, clicked, asked). Consultant persona, qualify and build trust. - closing — temperature ≥ 75 from sustained engagement. Closer persona, push for the concrete next step. - recovery — previously engaged and now cooled, OR explicit cooling signal (ghosted_*, not_interested, bad_timing). Re-engager persona, low-pressure re-open. - dead — asked not to be contacted (a reply that opted out, or the opted_out signal). Never message them again; get_message_prompt refuses. Counted as do_not_contact. Presentation: lead with the summary counts, then list the top prospects as a numbered list with handle, temperature, mode, and last signal. Do NOT introspect this response with shell commands — the data is already structured and ready to summarise inline. |
| track_prospect | Log a signal from a prospect and update their temperature. `signal` examples: booked_demo, asked_pricing, viewed_content, replied, not_interested, too_expensive, no_time, competitor, ghosted_3_days, ghosted_7_days, opted_out (they asked not to be contacted: ends the relationship). Creates the prospect if new. When you have the prospect's actual reply text, use `record_reply` instead: it reads the reply and updates temperature, mode, objections and do-not-contact for you. |
| get_message_prompt | Get the drafting payload for the next outreach message to a prospect. This is a working payload, not a display payload — it contains the full system prompt, recent conversation history, persona voice, and response schema. Use it to write the message; do NOT dump the prompt or conversation history back to the user. Workflow: 1. Call this with the prospect_id. 2. Compose a message that fits the persona and follows the schema. 3. Call `record_message(prospect_id, message, tactic, next_step)`. Daily send cap is enforced — record_message will return a cap-hit error if exceeded for the day. |
| record_message | Record a message you drafted as sent to the prospect. Use after `get_message_prompt`. Daily send cap still applies. |
| get_products | List configured products being monitored for buying signals. Each product includes: id, name, value_prop, target_audience, is_active, created_at. Presentation: list each product on its own line with name, status (active/inactive), and a one-line value prop. Do NOT dump the raw JSON — summarise inline. |
| suggest_anchors | Generate candidate anchor sentences for a product. CALL THIS BEFORE `add_product`. Anchors are the single highest-leverage field in the whole system — they are what every incoming post is compared against, so they decide what the user does and doesn't get shown. Writing them well is hard and most people write marketing copy by mistake, so use this and then let the user edit. Recommended setup flow: 1. Ask the user three things: what the product is, who buys it, and what it does for them. 2. Call this tool. 3. SHOW the returned anchors to the user and invite edits — they know their buyers' words better than any model does. 4. Call `add_product` with the approved list, then `reload_products`. A good anchor is the BUYER speaking about their PROBLEM, moments before they'd want this product ("my stream background is just a static image and looks boring", "we're paying for seats nobody uses"). A bad anchor is the seller describing the product ("premium audio-reactive visual engine"). Product-voice anchors match other sellers; buyer-voice anchors match buyers. Returns: {"anchors": [str, ...]} |
| add_product | Register a new product to monitor for buying signals. Call `suggest_anchors` FIRST and get the user's approval on the anchors — do not invent them silently. Then `reload_products` to activate. `anchor_sentences` (required, 3+, each 5-40 words) is the most important field in the system: every incoming post is scored by similarity to these. Write them as the BUYER describing their PROBLEM, not as the seller describing the product: GOOD "looking for an alternative to X, the pricing has gotten absurd" GOOD "anyone know a tool that does Y? doing it by hand is killing me" BAD "enterprise-grade Y automation platform" <- matches sellers, not buyers `target_audience` and `value_prop` — describe ONE buyer, narrowly: who they are and the problem they would write about. The judges read both fields when deciding whether a post's author is a buyer, so a broad audience (a list of groups, "any operator") makes them keep weak leads. If the product has clearly different buyers, add each as its own product. `buy_signal_keywords` — LEAVE THIS EMPTY unless you have a specific reason. It is an optional COST filter, not a quality one, and it is an AND-gate: a post matching NONE of the keywords scores 0 before embedding, before the swarm, with nothing logged. For example, the same sentence scored 0 when it said "people" and highly when it said "leads" — one word. Nobody can list every phrasing a buyer might use, so a keyword list silently deletes real buyers in proportion to how badly you guessed. The embedding floor, vertical gate and 3-judge swarm still filter without it. `competitor_keywords` — direct SUBSTITUTES only (tools a buyer would pick INSTEAD of this product). A buyer-intent mention of one floors the score so switchers always surface. Do not list adjacent-category tools that merely sound similar; you will surface people who were never in your market. |
| add_station | Add a feed for a product to monitor. Add 3-5; station CHOICE decides results. Valid `platform`: `rss`, `hn`, `reddit`, `twitter_search` (the last only routes outreach to X — it does NOT monitor X; X has no feed to read). URL patterns: Reddit: https://www.reddit.com/r/SUBREDDIT/new/.rss <- use /new/ HN: https://hnrss.org/newest?q=KEYWORDS Use `/new/.rss`, not the bare `/.rss`. Bare gives the HOT listing (popular, already-answered posts); `/new/` gives fresh ones, and scoring rewards recency heavily — the first helpful reply is the one that gets read. CHOOSING STATIONS — this matters more than any setting: * Prefer BROAD communities where buyers describe PROBLEMS, over narrow keyword or tool-name searches. Broad community feeds consistently produce the overwhelming majority of real leads; narrow keyword-search feeds frequently produce none at all. * Go where your buyer complains, not where your industry talks shop. Vendor/founder communities are dense with people BUILDING things like yours (correctly filtered out as sellers) and thin on buyers. * 3-5 well-chosen feeds beat 20. Every feed costs a fetch each cycle and sources rate-limit; more stations mainly buys you more noise. * Ask the user where their customers actually hang out and complain. They know; a guessed subreddit list is the most common cause of an empty queue. |
| reload_products | Reload the product cache after adding or editing products. |
| record_reply | Record what a prospect wrote back, and let the brain read it. Call this whenever a tracked prospect replies (a comment reply, a DM, an email). The brain reads the reply and updates their temperature, mode and objections, and returns: - intent: buy / interested / objection / not_now / unsubscribe / neutral ... - mode: the prospect's new stage (nurture / sales / closing / recovery / dead) - do_not_contact: true when they asked not to be contacted. Stop: never message them again (get_message_prompt will refuse). - invites_private_contact: true only when THIS reply explicitly asks to continue in private ("DM me"). Reddit and X require that consent before an app sends a private message, so never suggest a DM without it. Presentation: tell the operator the intent, the new mode, and any new objection in one line. If do_not_contact is true, say so plainly. |
| preview_station | Check whether a feed has buyers for a product BEFORE adding it as a station. Which communities you listen to decides results more than any setting, and a feed full of people talking about your topic can still contain no one asking to buy. This reads the feed, scores its fresh posts and has the panel judge the best-matching ones (default 8, one judgement each). Nothing is saved. Pass `rss_url`, or `entries` (up to 50 {link, title, summary, author, published}) that you fetched yourself. Feeds read on the operator's own machine (read_by: client) must be sent as entries. Returns a verdict fixed by pre-set thresholds: VIABLE (worth adding), MARGINAL (a trickle), ON-TOPIC, NOT IN-MARKET (topic talk, no buyers: a different community will do better), NO BUYERS, or NO DATA (nothing could be judged; the explanation says why, such as an empty or stale feed or keywords that blocked every post). Plus a sample of judged posts, each kept or rejected with the judges' stances. Presentation: lead with the verdict and its one-line explanation, then how many of the judged posts were kept. Only suggest add_station for VIABLE or MARGINAL. |
| list_stations | List your stations (feeds) with a health readout for the last two weeks. Each station: id, name, platform, rss_url, read_by (server = SignalPipe reads it, client = your plugin or daemon reads it), active, posts captured, how many reached your queue, empty or rate-limited fetches, and a plain-language verdict (for example "Posts are arriving but none matched your product: change the anchor sentences"). Presentation: one line per station with name, active or paused, and the verdict. Use the ids with update_station / remove_station. |
| update_product | Edit a product, or pause and resume it. Only the fields you pass change. anchor_sentences follow the add_product rules (3+, each 5-40 words, the BUYER describing their problem); the previous set is kept so a bad edit can be undone. active=false pauses the product: its stations stop being read and score_signal stops accepting it until you set active=true. Products are paused, never deleted, so their history stays. Confirm with the operator before changing anchors or pausing. |
| update_station | Pause, resume, rename or re-point a station. Only the fields you pass change. Get station ids from list_stations. A new rss_url is checked (http or https, a public host) before it is saved. |
| remove_station | Remove a station. Confirm with the operator first. A station that never produced a lead is deleted. One that already produced leads is switched off instead, to keep that history; the reply says which ("deleted", or "paused" with reason "has_history"). |