openmud
by bilhokista
README.md
# openmud
**Signal-first account research for go-to-market, built for AI agents.**
openmud finds companies that show a buying signal right now, reads their
public homepages, scores them against your rules, and writes a brief for
each one you should look at first. It runs as a CLI or as an MCP server, so
Claude Code, Codex or any MCP client can drive the whole thing.
It has no dependencies, no credits and no account. It reads public pages,
honours `robots.txt`, and **never sends anything to anyone**.
```console
$ openmud run hn -w runs/growth --match "growth|marketing|gtm" --remote -c examples/gtm.toml
29 accounts, 10 drafts -> runs/growth
$ ls runs/growth
accounts.csv enriched.csv enriched.coverage.md drafts.csv drafts.md run.json
```
```markdown
## Chronograph (chronograph.pe), score 50
- Why now: Chronograph (chronograph.pe) | Platform Engineer | Full-Time | Remote (US) | ...
- Source: https://news.ycombinator.com/item?id=49529957
- What they do: Chronograph's portfolio monitoring software empowers cloud-based analytics, ...
- Tech seen: GA4; HubSpot; LinkedIn Insight; WordPress
- Angle: hiring_signal == yes (+30) | tech missing Google Tag Manager (+10) | tech contains HubSpot (+10)
- Status: needs_review
```
## The pipeline
| step | command | what it does |
| --- | --- | --- |
| discover | `openmud discover hn\|yc -o accounts.csv` | accounts from a public signal, each row carrying the signal and its link |
| enrich | `openmud enrich accounts.csv -o enriched.csv` | domain (graded), homepage description, tech seen, careers page, fit score, coverage report |
| draft | `openmud draft enriched.csv -o drafts.csv` | a brief per top account, plus an opener only if a model can back it with a verbatim quote |
| run | `openmud run hn\|yc -w folder` | all of the above into one folder, with a `run.json` summary |
Every command takes `--json`: the human report goes to stderr and one JSON
object to stdout.
### Signal sources
- **`hn`**: this month's Hacker News "Ask HN: Who is hiring?" thread, through
the public Algolia API. Filters: `--match` (regex over the post),
`--remote`, `--thread`.
- **`yc`**: Y Combinator companies marked as hiring, from the
community-maintained [yc-oss/api](https://github.com/yc-oss/api) dataset
(refreshed daily). Filters: `--industry`, `--region`, `--batch`,
`--min-team`, `--max-team`.
Both need no key. A company hiring is one of the plainest buying signals
there is: budget exists, and someone is about to own a problem.
## Use it from an agent
```bash
pip install git+https://github.com/bilhokista/openmud
claude mcp add openmud -- openmud mcp # Claude Code
codex mcp add openmud -- openmud mcp # Codex CLI
```
Or as a Claude Code plugin, which brings the MCP server and the skill:
```
/plugin marketplace add bilhokista/openmud
/plugin install openmud@openmud
```
The MCP server exposes `openmud_discover`, `openmud_enrich`, `openmud_draft`
and `openmud_run`. There is no tool that sends, and there will not be one.
`skills/openmud/SKILL.md` tells the agent how to use them: cite the signal,
report coverage as measured, treat low-confidence rows as unchecked, and
leave the sending to a person. `AGENTS.md` covers the same for Codex and for
agents working on this code.
## Why trust the table
Most of the value in a GTM table is knowing which cells you can trust.
openmud is built around that:
- **Every row keeps the signal that put it there**: `signal`,
`signal_detail`, `signal_url`, `signal_date`.
- **Every value records its source.** `description__source` says whether
the description came from the site's own meta tag or from the AI column.
- **The coverage report is part of the output.** Each run prints how full
each column is and which source filled it, and saves that as JSON and
Markdown.
- **Anything a model writes must quote its source.** The AI description and
the opener are kept only when they come with a quote that appears word for
word in the evidence. An opener that mentions a number the evidence does
not contain is dropped too, which catches invented funding rounds and
growth figures. These are string checks and do not rely on the model
behaving.
- **Guessed domains are verified and graded.** A candidate domain is
accepted only if its homepage names the company and does not look parked
or for sale. Guesses on short names are marked `low` confidence.
- **Empty means unknown.** A `tech missing GA4` rule does not fire when no
page was read.
## Measured
Live runs on 24 September 2026, `examples/gtm.toml`, no AI columns:
| run | accounts | domain | description | tech | careers page |
| --- | --- | --- | --- | --- | --- |
| `hn --match "growth\|marketing\|gtm" --remote --limit 30` | 29 | 83% | 76% | 62% | 69% |
| `yc --industry B2B --region Remote --max-team 30 --limit 20` | 20 | 100% | 95% | 80% | 75% |
Domain guessing, checked by hand on an earlier 40-company list where only 11
arrived with a domain:
| confidence | guesses | confirmed right | wrong or unconfirmed |
| --- | --- | --- | --- |
| high | 17 | 15 | 2 |
| low | 5 | 3 | 2 |
Short, common names are where guessing fails, which is why they are flagged
and not hidden. Treat `low` rows as "check before use".
## Columns
| column | from | meaning |
| --- | --- | --- |
| `signal`, `signal_detail`, `signal_url`, `signal_date` | source | why the account is on the list |
| `domain`, `domain__source`, `domain_confidence` | input, row text, guess | where the domain came from and how sure we are |
| `title`, `description` | homepage | the site's own words |
| `tech` | homepage HTML | GA4, Google Tag Manager, Meta Pixel, HubSpot, Segment, Intercom, Hotjar, LinkedIn Insight, TikTok Pixel, Stripe, Shopify, WordPress, Webflow, Framer, Next.js. Only what the HTML shows; tools loaded inside a tag manager are not visible |
| `careers_url`, `ats`, `hiring_signal` | homepage links | a careers page or ATS link. A proxy for hiring, not proof of an open role |
| `description_quote` | AI column | the verbatim quote backing an AI description |
| `score`, `score_reasons` | your rules | fit score with the rules that fired |
`enrich` also works on your own CSV. It needs a `domain` column, which can be
empty when there is a `company` column: openmud then looks for a matching
URL elsewhere in the row, and then checks `company.com`, `.io`, `.ai`, `.co`
and `.dev`. `--no-guess` turns that off.
## Waterfall and scoring
`openmud.toml` maps each column to an ordered list of sources; the first
non-empty one wins.
```toml
[columns]
description = ["website.site_description", "research.summary"]
[[score]]
when = "hiring_signal == yes"
points = 30
```
Rules are parsed, never evaluated as code: `field == value`,
`field != value`, `field contains item`, `field missing item`,
`field present`, `field empty`.
## AI columns
Off by default. Point them at any OpenAI-compatible endpoint, including a
local model:
```bash
export OPENMUD_LLM_BASE_URL=http://localhost:11434/v1 # Ollama
export OPENMUD_LLM_MODEL=llama3.1
# OPENMUD_LLM_API_KEY=... # for hosted endpoints
```
The model only sees the homepage text (for the description) or the row's
evidence (for the opener), and its answer is dropped unless the checks pass.
## What it does not do
- It does not send email or messages. Research and outreach are separate
decisions, and the second one deserves a person.
- It does not find people or personal email addresses.
- It does not log into anything or read pages behind a login.
## Development
```bash
python -m venv .venv && .venv/bin/pip install -e ".[dev]"
.venv/bin/pytest
```
Tests never touch the network: every source and provider takes a fetch
function, and the tests pass a fake one.
MIT licensed.
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues