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.
$ 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## 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| 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.
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 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.
pip install git+https://github.com/bilhokista/openmud
claude mcp add openmud -- openmud mcp # Claude Code
codex mcp add openmud -- openmud mcp # Codex CLIOr 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.
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__sourcesays 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
lowconfidence. - Empty means unknown. A
tech missing GA4rule does not fire when no page was read.
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".
| 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.
openmud.toml maps each column to an ordered list of sources; the first
non-empty one wins.
[columns]
description = ["website.site_description", "research.summary"]
[[score]]
when = "hiring_signal == yes"
points = 30Rules are parsed, never evaluated as code: field == value,
field != value, field contains item, field missing item,
field present, field empty.
Off by default. Point them at any OpenAI-compatible endpoint, including a local model:
export OPENMUD_LLM_BASE_URL=http://localhost:11434/v1 # Ollama
export OPENMUD_LLM_MODEL=llama3.1
# OPENMUD_LLM_API_KEY=... # for hosted endpointsThe 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.
- 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.
python -m venv .venv && .venv/bin/pip install -e ".[dev]"
.venv/bin/pytestTests never touch the network: every source and provider takes a fetch function, and the tests pass a fake one.
MIT licensed.