Skip to main content
Glama
retrocodes12

Earshot

by retrocodes12
README.md
# Earshot

**Hear who needs your product, minutes after they say it.**

Earshot is a self-hosted buying-intent radar for the open social web. It sweeps Hacker News, Bluesky, Mastodon, Lobsters, Dev.to, Stack Exchange, GitHub and Discourse forums for conversations where someone is asking for a product like yours — scores each one for intent — and alerts you while the reply window is still open.

It never posts, replies, or DMs. Earshot finds the conversation; you show up as yourself.

[![Earshot in sixteen seconds: a buyer asks, the radar hears it, scores it, and explains why](.github/launch-preview.gif)](.github/launch.mp4)

## Why this exists

GummySearch had 135,000 users when Reddit's commercial API pricing killed it in November 2025. Every hosted replacement inherits the same failure mode: platform API bills that scale with *their* user count, until the bill wins.

A self-hosted radar breaks that loop. One instance, one user, polite polling of public endpoints — the economics that killed the hosted tools simply don't apply. Your data stays in a SQLite file on your machine.

## Quickstart

**Docker:**

```bash
git clone https://github.com/retrocodes12/earshot && cd earshot
docker compose up -d
# dashboard: http://localhost:3300
```

**Bare Node (≥ 22.5) — there is no `npm install`, the app has zero dependencies:**

```bash
git clone https://github.com/retrocodes12/earshot && cd earshot
node server.js
```

Then create your first watch list — in the dashboard, or from the terminal:

```bash
node bin/earshot.mjs watch "YourBrand" \
  --desc "One sentence: what you sell and who buys it." \
  --keywords "your category,competitor alternative,problem your buyers describe"
```

The radar sweeps every 3 minutes. Add `ANTHROPIC_API_KEY` to `.env` for real intent scoring (a built-in heuristic runs without it), and `RESEND_API_KEY` for email alerts (they print to the log otherwise). Full reference: [`deploy/env.example`](deploy/env.example).

## Give your agent ears (MCP)

Earshot ships an MCP server, so any MCP client — Claude Code, OpenClaw, Codex — can watch the radar and act on it:

```bash
claude mcp add earshot -- node /path/to/earshot/mcp/server.mjs
```

| Tool | What it does |
|---|---|
| `earshot_leads` | Flagged conversations, best first, with score / why / URL / reply-window flag; filter by theme |
| `earshot_ask` | Natural-language question over the archive, answer cited to conversations |
| `earshot_search` | Full-text search over everything stored |
| `earshot_watch` | Create a watch list from a product description |
| `earshot_scan` | Trigger a sweep right now |
| `earshot_stats` | Watch lists and store counts |

"Check my radar every morning, draft replies to anything above 80, and show me before sending" is a working agent loop today. The agent drafts — the reply still goes out as you. See [the never-posts principle](#principles).

There's a CLI with the same verbs: `earshot scan`, `earshot leads`, `earshot watch`, `earshot stats`, `earshot mcp`.

## How scoring works

Every new conversation is scored 0–100 for buying intent against *your* product description — not keyword matching, intent reading. `asking for recommendations` beats `mentioned the category` beats `wrote about the problem`. Competitor mentions raise the score and get named in the reason. Conversations that are young and still quiet get an **act-now** flag: replies inside the first two hours land very differently from replies the next day.

Mark a lead as noise in the dashboard and the scoring learns your project's shape.

With `ANTHROPIC_API_KEY` set, scoring runs on Claude (batched, budgeted, capped per sweep). Without it, a transparent heuristic does its best. Either way the score's *reason* is always shown — no black-box verdicts.

## The dashboard

<picture>
  <source media="(prefers-color-scheme: light)" srcset=".github/screenshot-light.png">
  <img alt="Earshot dashboard: scored conversations, best first, each with the reason it was flagged" src=".github/screenshot-dark.png">
</picture>

## The GummySearch feature map

GummySearch died with 135k users; Earshot rebuilds its working grammar on the open web. Where each piece lives:

| GummySearch | Earshot |
|---|---|
| Audiences (subreddit bundles + keywords) | Watch lists: brand + keywords + competitors per project |
| Themes: Solution Requests, Pain & Anger, Advice Requests, Money Talk | The same four categories, classified on every stored conversation — sidebar tabs with live counts. GummySearch's categories were curated intent-phrase sets; so are these, transparent and in [`src/themes.js`](src/themes.js) |
| Hot Discussions / Top Content | `sort=hot` (comment velocity) and `sort=top` views |
| AI analysis per theme | "Analyze this theme": recurring phrase patterns always, AI narrative with a provider key |
| Ask (natural-language questions) | Ask bar → `/api/ask`: ranked matches always, cited AI answer with a provider key |
| Keyword alerts | Instant email alerts + optional `EARSHOT_DIGEST=daily` roundup |
| Historical archive search | Search bar over everything stored, `/api/search` |
| 130k subreddits | The open web instead: 8 sources, no Reddit, and [that's the point](#why-this-exists) |

## Sources

| Source | Auth needed | Notes |
|---|---|---|
| Hacker News | none | Algolia public API |
| Bluesky | app password | raises rate limits |
| Mastodon | none | set instances via `EARSHOT_MASTODON_HOSTS` |
| Lobsters | none | |
| Dev.to | none | |
| Stack Exchange | none | pick sites via `EARSHOT_SE_SITES` |
| GitHub | token optional | issues/discussions search |
| Discourse forums | none | any instance via `EARSHOT_DISCOURSE_HOSTS` |

`EARSHOT_SOURCES=hn,bluesky` narrows the set without a code change. Adding a source is one small file implementing the adapter contract — see [`src/sources/`](src/sources/).

No Reddit: Earshot polls open public endpoints politely and stores no Reddit data. That's a licensing stance, not a technical gap — it's what keeps the self-hosted model clean.

## Principles

- **Earshot never posts.** It has no write path to any platform. The moment a tool auto-replies at scale, every community it touches gets worse, and so does the signal. You answer as yourself, or not at all.
- **Open web only.** Public endpoints, polite intervals, honest user-agent.
- **Your data is a file.** One SQLite database. Back it up with `cp`.
- **Readable verdicts.** Every score comes with its reason.

## Architecture

One Node process: HTTP server + poll loop, `node:http` and `node:sqlite`, zero npm dependencies. The poll loop never overlaps itself; sources are cursor-based so nothing is fetched twice. ~2,000 lines total. It runs comfortably in 256 MB.

```
server.js          HTTP + poll loop (one process, on purpose)
src/pipeline.js    sweep: fetch → dedupe → score → alert
src/score.js       intent scoring (Claude or heuristic)
src/sources/*.js   one small adapter per platform
mcp/server.mjs     MCP server (stdio, zero-dep)
bin/earshot.mjs    CLI
```

## Hosted mode

Everything above is the default, ungated self-host experience. The optional hosted mode (accounts with trials, billing-webhook plan gating) activates only when `EARSHOT_BILLING_URL` is set — self-hosters can ignore that it exists.

## License

[Apache-2.0](LICENSE)