Skip to main content
Glama
OpenHire — Real Job Postings, Ghost Jobs Scored

OpenHire — Real Job Postings, Ghost Jobs Scored

OpenHire · 开聘

Companies screen you with AI. Screen them back. 已读不回?先看岗位还在不在。

Let your AI assistant find jobs straight from employers' own systems, with your résumé never leaving your computer. Is the job still open? Is the company real? Check both before you apply. 让 AI 助手直接从雇主自己的系统里替你找工作,简历不出你的电脑。岗位还在不在,公司真不真,投之前先查一遍。

MCP 1.0 privacy: local-first python ≥ 3.11 license: MIT 216 employers hiring OpenHire on Glama

投了没回音,该等还是该走? 对你的 AI 助手说一句「我投的这 12 个岗位,哪些已经下架、不用再等?」,它从雇主自己的招聘系统里答(2026-10-26 索引上真跑的结果,节选): Applied and heard nothing? Ask your assistant "which of the 12 postings I applied to have closed?" and it answers from the employers' own systems (a real run on the 2026-10-26 index, trimmed):

你记下的 12 个岗位里:4 个已从雇主系统下架,1 个 30 天没再出现,7 个还在架。下架和长期不出现的,不用再等回复了。

closed   Torc Robotics   Software Engineer II - Device Drivers      已从雇主系统下架(索引在 2026-10-26 发现),不用再等这一个了。
closed   Figure          Lead, Data Operations Global Expansion     已从雇主系统下架(索引在 2026-10-12 发现),不用再等这一个了。
closed   越疆 Dobot      具身智能项目经理(J10310)                   已从雇主系统下架(索引在 2026-10-26 发现),不用再等这一个了。
stale    ClickHouse      Core Software Engineer (C++) - Remote      雇主系统 30 天内没有再返回这个岗位,多半已不在招,但我们没法确认。
open     Waymo           Senior Business Intelligence Analyst       还在雇主系统上(已在架 149 天)。还在架不代表会回复,只说明岗位没撤。
open     Physical Intelligence  Mechanical Design Engineer          还在雇主系统上(已在架 503 天)。还在架不代表会回复,只说明岗位没撤。

It tells you whether the posting is still there, never whether the employer read you: no index can see an inbox. 只说岗位在不在,不说雇主看没看:谁也看不到雇主的收件箱。投的时候也不交简历给任何服务器。

In Claude Code, the plugin is a whole local job-search workflow: find postings, check the company, tailor your CV, build interview stories, follow what you applied to. Your CV stays in your own folder (cv.md) and is read by your own assistant; OpenHire tools only ever get a job id. /plugin marketplace add gzchenhao/openhire, then /plugin install openhire@openhire. 在 Claude Code 里装上插件,就是一套完整的本机求职工作流:找岗、查公司、改简历、备面试故事、盯投递,简历留在你自己的文件夹里,OpenHire 的工具只拿到岗位编号。

Every number here is checkable. Open-duration report (per-employer medians, named, monthly) · numbers.json (the provenance of every figure we quote, regenerated each refresh) · raw report data. 我们引用的每个数字都能核:岗位在架时长月报 · numbers.json

Is your company in here, and you did not put it here? Your postings are in this index because your own careers page serves them publicly. We can measure how long a role has been open; we cannot see why, and a long-open role is a question, not a verdict. Claim your company — free, no payment, ever — and say why in your own words, or ask us to remove you and we will, without arguing. 贵公司被收录了、而且不是贵公司提交的?点这里认领,免费, 可以用自己的话解释,也可以直接要求我们移除。

三不原则 · Three things OpenHire never does

  1. 不存简历 · No résumé, ever. 简历留在你的电脑上。过网的只有一个你自己生成的匿名指纹和岗位号;协议里没有简历字段,多传一个 resume 参数会被拒绝,CI 测试钉死。 Your résumé stays on your machine. Only an anonymous, client-generated fingerprint and a job id ever cross the wire; the protocol has no résumé field, an extra resume argument is refused, and CI pins it.

  2. 不刷假单 · Never applies for you. 它只把你送到雇主自己的投递页,从不向任何 ATS 提交申请;连表单长什么样,我们的测试也只用一次普通 GET 看,到此为止。 It hands you the employer's own apply page and never submits anything to any ATS. Even our own tests only GET the form once and stop.

  3. 不追踪 · No tracking. 不埋点、不统计谁看了什么。服务器只记一条匿名授权(指纹 + 岗位号),没有你是谁;索引本身跑在你自己的机器上。 No analytics, no view counting, no telemetry. The server keeps one anonymous authorization record (fingerprint + job id) and nothing about who you are; the index itself runs on your machine.

还有一条不在这三条里,但同样锁死:排序买不到,它是(匹配度,新鲜度)的纯函数,见下文三条隐私红线。 One more that is locked the same way: ranking cannot be bought; it is a pure function of (match, freshness). See the three red lines below.

What your agent actually sees

You ask your assistant a question in plain language. It calls search_jobs, and every row comes back carrying the employer's real posting date — so the agent can reason about staleness instead of guessing.

You: Any senior Python roles that are actually still open? Skip the stale ones.

// one row from search_jobs — trimmed to the fields that matter here
{
  "title":        "Senior Python Engineer",
  "company":      "MongoDB",
  "datePosted":   "2026-03-31",   // from the employer's ATS, not a board's refreshed label
  "days_open":    166,
  "ghost_score":  0.61,           // pure f(relist_count, datePosted) — frozen by a test
  "apply_channel":"https://boards.greenhouse.io/…",   // straight to the employer
  "verified_at":  "2026-09-02T09:47:10Z"
}

Assistant: This one has been open 166 days with a ghost_score of 0.61 — I'd deprioritise it. Here are four posted in the last three weeks instead…

ghost_score measures how long a posting has been open, not whether the employer still intends to hire. A long-open role can equally mean "hard to fill". Treat it as a reason to ask, not a verdict.


An MCP server that turns any MCP-speaking assistant — Claude, 豆包 Doubao, DeepSeek (through an MCP client such as Cherry Studio or Chatbox), Cursor, 腾讯 WorkBuddy, TRAE, Windsurf, Cline, ChatGPT (through the hosted endpoint below) — into a private radar for AI / Infra, autonomous-driving and embodied-AI jobs — pulled straight from 216 employers' own career sites and public ATS APIs (Greenhouse / Lever / Ashby / 北森 Beisen / Moka, plus first-party employer career sites such as Li Auto's), across the US, Europe and China (Waymo, Figure, Zoox — and Unitree, XPeng, UBTECH, Mech-Mind…). No account. No signup. No résumé upload. Ever.

Three things a job board won't do for you:

  • Surfaces how long each role has really been open. Every listing carries a ghost_score aged off the employer's real posting date where the employer's system reports one (otherwise the day this index first saw it, flagged date_signal): the "2 days ago" a board shows you can be 300 days old in the ATS.

  • Structural privacy, not a pinky-promise. There is no résumé field in the protocol; a CI test fails the build if anyone adds one. Matching runs on your machine — only an anonymous fingerprint reaches the server.

  • Ranking you can't buy. Order is a locked pure function of (match, freshness). No sponsored slots, no bidding — the signature is frozen by a test.

This is the 「哨兵 / Sentinel」 reference implementation — see design_handoff_openhire_v01/README.md for the full protocol spec.


Related MCP server: Job Application MCP

Quickstart — under a minute

Nothing to install, and no crawl to sit through. Add this to your MCP client's config:

{ "mcpServers": { "openhire": { "command": "uvx", "args": ["openhire@latest", "serve"] } } }

Then ask your assistant for a job. That is the whole setup. The server downloads the ~30 MB public index by itself in the background on first start, so searches fill in within a couple of minutes while you are already talking to it. No account, no signup, no résumé upload.

Not sure which of these steps are yours? The connect guide (国内:openhire-vlny.onrender.com/connect) asks which assistant you use (ChatGPT, Claude, 豆包, DeepSeek, Cursor, 腾讯 WorkBuddy, VS Code) and shows only that one's steps, with the one block to paste. 选你用的助手,只看那一种的步骤。

Or paste this one block and let the assistant do the install and the first search (the check in the middle matters: an assistant that has just added a server usually cannot use it until it restarts, and left alone it will fake the result with a web search):

帮我装一个 MCP 服务:名字 openhire,启动命令 uvx openhire@latest serve。如果电脑上没有 uv,先帮我装 uv。

装好后检查:你现在能不能调用 openhire 的工具(比如 search_jobs)?
- 如果能,直接接着做下面的找工作任务。
- 如果不能(需要重启或开新对话才能加载),就停下来,告诉我该怎么重启,不要用网页搜索或别的办法代替。我重启后会把这整段再贴一次。

找工作任务:我的情况:[目标城市],想做[岗位方向,例如 AI 产品经理 / 感知算法 / 机器人运营],[几年]经验,会[3 到 5 个技能],不考虑[不要的方向]。
1. 用 OpenHire 搜索匹配的在架岗位,每条带公司、地点、原始发布日期、在架天数和投递链接,最近发布的排前面。
2. 在架时间特别长的岗位单独列出来提醒我,但不要替我下结论说它不招了。
3. 搜不到的公司,用门户目录告诉我它们的招聘页在哪里。
4. 帮我把这个求职意向设成盯岗,以后我说「检查一下」,就告诉我新出现了哪些岗位。
5. 我挑中哪家,就用企业背景查证帮我把这家公司查一遍。

我的简历只留在我电脑上,不要发给任何服务器。投递由我自己点链接去做。
Install an MCP server for me: name openhire, command uvx openhire@latest serve. If uv is missing on this machine, install uv first.

Then check: can you call openhire's tools right now (search_jobs, for example)?
- If yes, go straight on to the job-search task below.
- If not (a restart or a new chat is needed to load it), stop and tell me how to restart. Do not substitute a web search or anything else. I will paste this whole block again after restarting.

Job-search task. About me: [city], looking for [role, e.g. AI product manager / perception / robotics operations], [N] years, skills [3 to 5], not interested in [what to exclude].
1. Search OpenHire for matching live postings: company, location, original posting date, days open and apply link, newest first.
2. List the postings that have been open unusually long separately, without concluding they are no longer hiring.
3. For employers it cannot find, use the portal directory to tell me where their careers pages are.
4. Register this intent as a watch; when I say "check", tell me what is new.
5. When I pick one, run the employer check on it.

My résumé stays on my machine; never send it to any server. I apply by clicking the links myself.

What to say to it. No tool names needed; plain sentences work. Paste any of these:

帮我找上海的感知算法岗,列出原始发布日期和在架天数
盯住 OKX 的招聘页,有新岗告诉我
把 https://jobs.lever.co/<公司> 加进我的索引,以后每周帮我刷新
哪些具身公司的岗位在飞书上?把它们的招聘页给我
查一下这家公司靠不靠谱:<公司名>
投之前帮我查一遍这个岗位:<公司名> <岗位名>
我是 2027 届应届生,帮我找具身公司收应届的岗位,写清学历要求
别让我海投:帮我挑 10 个值得认真投的智驾岗,剔掉的说明原因
我投过的那几个岗位,哪些已经下架了,不用再等?
智驾和具身公司现在在招哪些 HR 和招聘岗?列出公司、城市和发布日期

Find perception roles in Shanghai with their original posting dates
Watch OKX's careers page and tell me when something new appears
Which embodied-AI employers post on Feishu? Give me their careers pages
Before I apply, check this posting: <company> <title>
I graduate in 2027; find robotics roles open to new grads and say what degree each asks for
Which of the postings I applied to have closed, so I can stop waiting?

Clients that show MCP prompts (Claude Desktop's "+" menu, a slash command in Claude Code or VS Code) also list twelve ready-made ones: 求职清单 · Job-search checklist, 找工作雷达 · Job radar, 盯一家公司 · Track one employer, 查这家公司靠不靠谱 · Check an employer, 投之前查一遍 · Check before you apply, 海投改精投 · Shortlist, don't mass-apply, 评估这个岗位 · How a posting fits me, 按岗位改简历 · Tailor my CV, 面试故事库 · Interview story bank, 打招呼草稿 · Outreach draft, 看看我的雷达 · What's new on my radar, and 本周新岗 · New this week. Each one renders in Chinese when you type Chinese into it.

New graduates (应届生) start here. Say what year you graduate and which sector; the assistant passes entry_level: true, which keeps only postings that say campus hiring, internship or no experience required, or whose title says intern or junior. Every result carries requirements: the lowest degree the posting requires (a degree it only prefers does not count), the fewest years it asks for, and whether it is a campus or intern track, read from the posting's own words. It also carries visa_sponsorship (offered / not_offered / null) with the posting's own sentence, so a seeker who needs a work visa sees what the posting says and asks the recruiter when it says nothing. On the 2026-11-03 index that is 1074 live postings (numbers.json, live_postings_open_to_new_grads). 应届生直接说「我是 2027 届,找收应届的具身岗位」:助手会只留写明校招、实习、经验不限,或岗位名带实习 / 初级的岗位, 每条都写出启事里要求的最低学历(只写「优先」的不算要求)和最少年限,不用一条条点开 JD 找「3 年以上」。 没写的就是没写,不等于不要求。

Three things a first-time user gets without asking:

  • A free job-search checklist: every step from target to offer, what the AI does for you, where you do the rest, and the three anti-scam checks, with the official addresses. Bookmark it: addresses move and scams change, and this page is kept current. Chinese and English, switch at the top right. gzchenhao.github.io/openhire/checklist.html 找工作每一步该做什么、哪步能让 AI 替你做、查公司和防骗去哪个官网,一页写全,免费,不用注册: openhire-vlny.onrender.com/checklist(国内直接开)

  • HR and recruiting roles have their own family (role_family: hr): a recruiter, HRBP or people-ops person can search their own market instead of digging through "ops". HR、招聘岗单独成类,做 HR 的人也能直接搜自己的岗。

  • Works from mainland China without a proxy: when GitHub is unreachable the index downloads from a mirror on CNB (Tencent) by itself, same file, same checks. 国内网络不用代理:GitHub 不通时自动从腾讯 CNB 镜像拿索引,同一份数据。

The one prerequisite is uv (it provides uvx). Without it the config above fails with nothing but "server failed to start", so install it first:

curl -LsSf https://astral.sh/uv/install.sh | sh     # macOS / Linux
irm https://astral.sh/uv/install.ps1 | iex          # Windows PowerShell

On Claude Desktop you can skip even that — download openhire-0.8.58.mcpb and double-click it. No terminal, no Python, no uv.

On Cursor, one click (it still needs uv on your PATH): Install MCP Server in Cursor

Per-client config paths and the trade-offs of uvx vs a one-time install are in Works with below.

pipx install openhire      # keeps it isolated and puts `ohp` on your PATH
ohp bootstrap              # 216 employers · ~28k live postings · no account

ohp search --required-skills rust,k8s --remote --role-family engineering
ohp search --currency CNY --role-family engineering   # e.g. CN 智驾 / robotics roles

ohp bootstrap downloads the public snapshot (~30 MB, no account) and then runs one incremental crawl to refresh verified_at and catch delistings. The crawl is the slow part — a line per employer, 20+ minutes on a cold index — and you can stop it once the snapshot is in; the index is already usable, just verified as of the last weekly refresh rather than today. The MCP path above needs none of this: serve fetches the snapshot by itself in the background.


Works with

All clients use the same MCP entry. The config below works in every MCP client and pulls the package on demand — but it does need uv present first.

New to MCP? Two shortcuts before the config below. Claude Desktop: download openhire-0.8.58.mcpb and double-click it. No terminal, no Python. Cursor — Install MCP Server in Cursor (needs uv installed). 腾讯 WorkBuddy / Cursor / Claude Code / TRAE — paste the one-block prompt from the Quickstart above: it installs, checks the tools are callable, and does the first search. ChatGPT (paid plans) — Settings → Connectors → Advanced → Developer mode → Create, name OpenHire, URL https://openhire-vlny.onrender.com/mcp, no authentication. Nothing to install; that host sees only tool arguments, never a résumé (see docs/PRIVACY.md §3). 豆包(豆包工作) — its custom connectors take an HTTP address, not a command: run ohp serve --transport streamable-http --port 8000 on your machine, then add http://127.0.0.1:8000/mcp as a 自定义连接器 (see the 豆包 entry under Works with). DeepSeek — the DeepSeek app has no MCP slot; use DeepSeek inside an MCP client (Cherry Studio, Chatbox, Cline, WorkBuddy, TRAE) and give that client the config below. VS Code (GitHub Copilot) — Install in VS Code (needs uv), or the .vscode/mcp.json under Works with below.

Prerequisite: uvx ships with uv. Without it the config below fails with nothing but "server failed to start" in your client — install uv first:

curl -LsSf https://astral.sh/uv/install.sh | sh     # macOS / Linux
irm https://astral.sh/uv/install.ps1 | iex          # Windows PowerShell
{ "mcpServers": { "openhire": { "command": "uvx", "args": ["openhire@latest", "serve"] } } }

First start downloads a ~30 MB index in the background; searches fill in within a few minutes.

Stuck? Run ohp doctor. It checks the three things that all look identical from the chat window — uv missing, no index yet, server configured but not enabled — and reads every client config it can find. It runs in your terminal, which matters: if the client never started our server, nothing we wrote inside it can reach you.

Editing the config may not be the last step. Several clients require you to enable or trust a newly added server before its tools load — the config is saved, the server never starts, and the only symptom is that your assistant does not seem to know about the tools. If a search does nothing, open your client's MCP/connectors panel and check that openhire is listed and switched on. (Claude Desktop needs a full quit and reopen; Cursor and Windsurf pick it up on reload; some clients show a per-server toggle.) 改完配置不一定就完事:部分客户端需要你在设置里手动「信任 / 启用」这个 server, 工具才会加载。症状是配置明明在、助手却完全不知道有这些工具。

What uvx costs you, every time. uvx resolves the package on each invocation — measured at 7–8 s per call even with a warm cache. That is paid on every MCP session start and every CLI command. It buys you never having to manage an install. If you would rather pay once:

pipx install openhire     # then use "command": "ohp", "args": ["serve"] — process-start latency

@latest also means your tool surface can change under you without warning. Pin it when that matters: "args": ["openhire==0.8.58", "serve"].

The server auto-downloads the public job snapshot on first run if the index is empty, so ohp bootstrap is optional. If you ran pipx install openhire, "command": "ohp" works too.

Claude Desktop — %APPDATA%\Claude\claude_desktop_config.json (macOS: ~/Library/Application Support/Claude/); quit & reopen after editing:

{ "mcpServers": { "openhire": { "command": "ohp", "args": ["serve"] } } }

Cursor — ~/.cursor/mcp.json (or a project .cursor/mcp.json):

{ "mcpServers": { "openhire": { "command": "uvx", "args": ["openhire", "serve"] } } }

Windsurf — ~/.codeium/windsurf/mcp_config.json:

{ "mcpServers": { "openhire": { "command": "uvx", "args": ["openhire", "serve"] } } }

VS Code (GitHub Copilot) — .vscode/mcp.json in your project (VS Code's own servers shape), or the one-click link in the shortcuts above:

{ "servers": { "openhire": { "type": "stdio", "command": "uvx", "args": ["openhire@latest", "serve"] } } }

Open the file and click Start above the JSON, switch Copilot Chat to Agent mode and tick openhire in the tools list; then paste the one-block prompt from the Quickstart. A tester who came in this way found no VS Code instructions here at all (reports/098), hence this entry.

Claude Code — this repo is also a plugin marketplace, so two commands inside Claude Code do the whole setup (still needs uv on PATH):

/plugin marketplace add gzchenhao/openhire
/plugin install openhire@openhire

Cursor plugin / Agent Plugins — the repo carries .cursor-plugin/plugin.json and the vendor-neutral plugin.json + mcp.json (agent-plugins.org), so any client that loads Agent Plugins can install it from the repo URL.

TRAE(字节) — one-click import links (TRAE asks you to confirm the config before adding it): TraeCode 国内版 · TRAE international. Or paste the uvx config above into TRAE's MCP settings by hand.

腾讯 WorkBuddy — ~/.workbuddy/mcp.json, same mcpServers shape. WorkBuddy reads the file when it starts, so after editing it quit and reopen the app (or add the server through the app's 专家·技能·连接器 → 自定义连接器 button instead); a hand-written entry does not appear in the list until then. No uv on the machine? pip install openhire into any Python ≥ 3.11 environment and point command at that environment's ohp executable with "args": ["serve"].

豆包 · 豆包工作(字节) — 豆包的「技能·连接器·伙伴 → 新建 → 新建自定义连接器」只收 HTTP 地址, 不收命令行。先在本机起一个 HTTP 端:

pipx install openhire
ohp serve --transport streamable-http --port 8000      # 本机 http://127.0.0.1:8000/mcp

然后在豆包里新建自定义连接器:传输类型 HTTP,服务器 URL http://127.0.0.1:8000/mcp,Headers 留空,保存后打开开关。 索引、指纹和匹配都在你自己的电脑上,豆包只是通过本机端口调用它。 (2026-10-12 在豆包工作里实测通过:新建连接器后直接问「用 openhire 搜香港的岗位」就有结果。)

DeepSeek — DeepSeek 自己的 App 没有 MCP 入口。把 DeepSeek 当模型用在任何支持 MCP 的客户端里 (Cherry Studio、Chatbox、Cline、腾讯 WorkBuddy、TRAE 都行),再把上面那段 uvx 配置填进该客户端的 MCP 设置即可;Cherry Studio 里是「设置 → MCP 服务器 → 添加 → 类型 STDIO,命令 uvx,参数 openhire@latest serve」。

First start downloads the ~30 MB public snapshot (jobs/companies only) — give it a moment. To refresh later run ohp bootstrap --force or ohp ingest. On Windows Claude Desktop from the Microsoft Store, the config is under …\Packages\<Claude package>\LocalCache\Roaming\Claude\.

Hosted endpoint, for ChatGPT and any client that only takes a URL (since 0.8.11): the maintainer runs a copy of this server at https://openhire-vlny.onrender.com/mcp (streamable HTTP, no authentication; a Render web service built from hosted/ via render.yaml, same package, same public index; /healthz tells you it is up and which version. It is a free instance: the first request after a quiet spell can take up to a minute). ChatGPT (paid plans): Settings → Connectors → Advanced → turn on Developer mode → Create: name OpenHire, URL above, authentication none; then ask in a new chat with the connector enabled. What that host receives is only what the tools take (search filters, a company name or domain, a board URL, the anonymous fingerprint), never a résumé; it keeps no query log; watches live in its index keyed by your fingerprint. The local install above remains the recommended path, because there nothing of a search leaves your machine. Details in docs/PRIVACY.md §3.

Self-host the same thing: OPENHIRE_HOSTED=1 ohp serve --transport streamable-http --host 0.0.0.0 --port 8000 exposes http://host:8000/mcp plus a landing page, /healthz, a per-address rate limit and a worker that picks up the weekly snapshot (also --transport sse). A Dockerfile is included.

GitHub unreachable from your network (mainland China without a proxy, some sandboxes)? When the GitHub download of the index snapshot fails, the server and ohp bootstrap try a copy on CNB, Tencent's code hosting, under the same checks (the copy is refreshed from GitHub every 6 hours by the repository's .cnb.yml; direct link https://cnb.cool/gzchenhao/openhire/-/releases/download/snapshot/openhire-index.db.gz , and OPENHIRE_SNAPSHOT_URL set to it skips GitHub altogether). The package itself comes from PyPI; a PyPI mirror makes the install fast, e.g. add "env": {"UV_DEFAULT_INDEX": "https://pypi.tuna.tsinghua.edu.cn/simple"} to the server's config (older uv: UV_INDEX_URL), or pip install -i https://pypi.tuna.tsinghua.edu.cn/simple openhire. The code is mirrored at https://cnb.cool/gzchenhao/openhire . ohp bootstrap --fresh skips the snapshot entirely and crawls the employers' own ATS (20+ minutes, no GitHub). An empty index caused by a failed download says so in the tool result (bootstrap_error) instead of looking like "no matches". 国内网络:GitHub 下载快照失败时,服务器和 ohp bootstrap 会改试国内镜像 CNB 上的副本(每 6 小时与 GitHub 同步,校验相同)。 镜像直链:https://cnb.cool/gzchenhao/openhire/-/releases/download/snapshot/openhire-index.db.gz ;在 MCP 配置的 env 里把 OPENHIRE_SNAPSHOT_URL 设成它,就完全不走 GitHub。 装包慢就用 PyPI 镜像:在 MCP 配置里加 "env": {"UV_DEFAULT_INDEX": "https://pypi.tuna.tsinghua.edu.cn/simple"} (旧版 uv 用 UV_INDEX_URL),或 pip install -i https://pypi.tuna.tsinghua.edu.cn/simple openhire。 代码镜像:https://cnb.cool/gzchenhao/openhire 。


What it does

Tool

What it gives you

search_jobs

Hard-filter the live index; every result carries verified_at, datePosted, days_open, ghost_score, remote_scope, eligible_regions, apply_channel. Filter by required_skills (AND), role_family (engineering, data, product, design, marketing, sales, hr for recruiting / HRBP / people roles, ops, other), remote_scope, min_salary + currency, company, location (bilingual city aliases: 香港 = Hong Kong, 广州 reaches 天河区), title (words in the job title, e.g. 招聘 / recruit — the filter for HR, finance and other roles no skill tag names) posted_within_days (only postings the employer published in the last N days) and seniority (ic / management, or intern, junior, senior, staff, lead, manager, director, unknown: the level the title names, so an engineer's search does not open with a Senior Manager). Each row also carries seniority, date_source, dates_in_words (the dates as one sentence, zh and en) and check_before_apply (what to confirm before applying, from measured facts).

watch_intent

Register a standing intent once — new matching jobs are waiting next time you check, even after you close the terminal. Accepts required_skills / role_family / location / title / seniority so sales / solutions / management roles stay out. Returns matching_now and what_happens_next: OpenHire never pushes; you ask the assistant to check, and the first check is a baseline of postings already live.

check_watches

Pull the matches that are new since your last check (client-pull; stdio has no push).

salary_benchmark

What employers themselves publish as pay for comparable live postings: low and high ends separately (p25 / median / p75), by currency, role family, level, years asked, sector and place; no quantiles under 20 postings. Published ranges, not offers; most Chinese postings publish none. 同类岗位雇主自己标的薪资范围,下限和上限分开报。

check_applications

An answer to "I applied and heard nothing": follows the postings you applied to (add their job_ids; authorized ones are included) and says, from the employer's own system, which are closed (stop waiting), which have not been seen for 30 days, and which are still live. Only job ids and your anonymous fingerprint are stored.

authorize_application

One explicit confirmation per job. It records your authorization and returns the employer's own application URL — you apply as yourself. It cannot accept a résumé.

get_company_info

Aggregate, anonymous trust signals for one employer (ghost_score_avg, active_jobs, index_built_at, and hiring_trend: postings published in the last 30 days against the 30 before, and postings taken down, so you can see whether it is growing or shrinking before you sign). Never any candidate data.

check_employer

Facts about one employer with sources and times: whether its own system is in the index, domain age (RDAP), site history (Wayback), the ICP record on its homepage, scam red flags in a pasted posting, the OFAC sanctions lists, the organisational and financing events on EarlyTip's public record, and the company register with your own 天眼查 key (TIANYANCHA_API_KEY, never ours). A checklist, never a score.

read_board

Read any employer's public board live from its careers URL (Greenhouse, Lever, Ashby, Moka, 北森, Workable, SmartRecruiters, Workday, Rippling, Personio; any sector; indexed or not): the board's own dates, direct apply links, location / title filters. Nothing stored; closed portals refused with the reason.

Optional, entirely local: ohp init --scan <dir> derives a skill fingerprint from your own repos. You never write a résumé; the code never leaves your machine — only an anonymous vector does.

For employers: claim your tenant

If your company is in this index, the listings came from your own public ATS — we did not ask, because we did not need to. What we cannot know is your side of it: whether a role is an evergreen talent pool rather than a stale req, or how fast you actually reply.

Claim it (中文表单). Free, verified by corporate identity — a GitHub org membership or a reply from a corporate domain — and never by payment. We answer within 3 business days, claiming leads to no paid follow-up of any kind, and your proof is used to verify and then nothing else.

No GitHub account? Email gdchenhao@qq.com with "Employer claim" and your company name in the subject — sending from your corporate domain is the first step of verification (enough for a claim; a self-reported roster is also checked against the company register, the ICP record and a callback to your public number) — or have anyone file the form on your behalf, since what we verify is the company and not the filer. 没有 GitHub 账号?直接发邮件到 gdchenhao@qq.com,用贵公司企业邮箱发出来即完成身份核验。

No ATS, or on Feishu Recruitment? Then your postings are not in the index yet and there is nothing to claim: the index reads employers' own systems only. Three read-only, free ways in, none of which touch ranking. The simplest needs no IT staff: fill in one spreadsheet and email it from your corporate domain (we then check the company register, the ICP record, the domain's age and call the public number on your own site); each posting is marked employer_self_reported, carries date_signal: self_reported, and expires after 90 days unless you send the sheet again. The other two: grant the two read-only Feishu scopes (hire:site:readonly, hire:site_job_post:readonly), or put machine-readable postings on your own careers page (schema.org JobPosting JSON-LD or a static JSON file). The claim form has a field for which of these fits you, and the employer page walks through all three. 没有招聘系统或用飞书招聘?最简单的一条:填一张岗位表,用企业邮箱发给我们,不需要 IT 人员;另外两条是飞书只读授权和官网机器可读岗位页,都写在认领表单和雇主页里。

A claim gets you:

  • Your own note, attributed to you, beside the roles you name. We can see how long a role has been open; we cannot see why. An evergreen talent pool, a genuinely hard-to-fill role, and a neglected one look identical from outside, and only you can tell them apart.

  • evergreen / hard to fill / closed status on specific titles. A closed role stays visible while your ATS still serves it — we do not hide what your own site returns — but it is marked closed on your word so nobody else applies.

  • A correction if your ATS's date field means "requisition opened", not "went live". That one misreading makes every role you have look years old, and it is not your fault.

  • response_sla_days on every one of your postings, including ones you post later

  • claimed: true on your company, with the date

It does not get you rank, and it does not lower your ghost_score. Ordering is a locked pure function of (match, freshness) and the score is a pure function of (relist count, posting age); tests freeze both and assert the claim path touches neither. Your note sits beside the score and explains it. We would rather show a high score with your explanation than a quiet score somebody paid for.

Asking to be removed entirely is also fine, and we will not argue about it.

Verified claims live in src/openhire/seed/claims.py — in the repo, not in a private database — so each one is a reviewable diff, and the weekly rebuild re-applies them instead of quietly dropping them.

Keeping it current

The index refreshes weekly, so a search can be up to seven days behind. When the user is about to act on one employer and wants today's truth:

ohp refresh unitree          # ~1 minute · at most one crawl per employer per 6 hours

Over MCP this is the refresh_index tool. Three rules are built in, not advisory:

  • One employer per call. A full crawl is 20+ minutes and no client waits that long. An ambiguous word (robot → 11 matches) is refused with the candidate list, never fanned out.

  • Six-hour throttle per employer, checked before any network call, so a too-soon request costs the ATS nothing and returns last_refreshed_at instead of an error.

  • Not for speculative or looped calls. Each one hits somebody else's public endpoint.

That last point is the whole design constraint: our own crawl is a weekly batch we control, and handing refresh to callers turns it into our users hitting their endpoint on our behalf. The throttle is what keeps that boundary ours to keep rather than ours to spend.

Reading the three dates

Every row carries three timestamps that answer three different questions. Read together they separate an abandoned requisition from one somebody is still tending:

field

question it answers

verified_at

did the employer's ATS still return this the last time we looked?

datePosted / days_open

how long has it been open? ghost_score ages off this. The employer's own date where its system reports one; otherwise the day this index first saw the posting, and the row carries date_signal: "not_reported_by_ats" (Li Auto's first-party mirror reports no date), so read it as a lower bound

updated_at / days_since_update

when did the employer last touch it? Null when their ATS does not report one (Ashby, Lever and Beisen do not; Greenhouse and Moka do) — read null as "unknown", never as "abandoned"

ghost_score = 1.0 alone is not a verdict. Open 367 days and untouched for 367 days reads as abandoned; open 327 days but touched 13 days ago reads as a tended evergreen req. Among the rows where the ATS actually reports a last-touched date, 67% of ghost_score >= 0.99 postings were touched by the employer within the last 30 days (measured 2026-09-19; the live figure is pct_ghost_hi_touched_within_30d in docs/numbers.json, which is regenerated every refresh). ghost_reason spells out which input drove the score ("age only: open 367d, never relisted").

None of these measure intent. A long-open role can equally mean hard-to-fill — treat the numbers as a reason to ask, not a verdict.

Is this employer what it says it is?

check_employer (CLI: ohp check-company 宇树 --domain unitree.com --jd posting.txt) returns a checklist, each item verified / not_verified / unavailable / not_applicable / pending_user with its source and time: whether the employer's own system is in the index and what it shows, the domain's registration date, the earliest Wayback capture, the ICP 备案号 on the homepage, and the recruitment-scam red flags in a posting you paste (or, with job_id, in the posting as this index holds it, so the assistant can check the role it just found without copying text). The company register (成立日期, 经营状态, 参保人数) costs money: set TIANYANCHA_API_KEY on your own machine and the tool fetches it with your 天眼查 account, or follow the free manual links it lists; for a Hong Kong or overseas employer those are the HK Companies Registry e-Services search, the HK Police Scameter (a phone number, URL or receiving account against the police scam database) and OpenCorporates. It also screens the name against the OFAC SDN and consolidated sanctions lists, downloaded to your machine daily and matched locally, because a live registration proves little on its own: Prince Group's Hong Kong companies were on the register the day they were sanctioned. Since 0.8.8 it also reads EarlyTip's public export of organisational and financing events (executive departures, restructurings, funding rounds, acquisitions, bankruptcies; earlytip.com/data/org_events.json), downloaded whole to your machine once a day and matched there by the export's own published company_key, and lists what is on record for the employer as a type, a date, the amount where recorded, a source domain and a plain link: never the headline, no link at all where the source site's terms list linking among the prohibited acts, retracted events dropped, and a note that EarlyTip checked its values against its own published headline and summary, not the source article. There is no total and no verdict on purpose: a young domain or a missing ICP record is a fact to weigh, not a sentence. An unavailable item says whether the source was reached and had no record, or whether this query never reached it (reason: network), because a check that could not run says nothing about the employer.

An employer outside the index

read_board (CLI: ohp read-board https://jobs.ashbyhq.com/uipath --location "Hong Kong") reads one employer's public board live, whether or not the employer is in the index and whatever its sector, as long as the board sits on a vendor the adapters already read: Greenhouse, Lever, Ashby, Moka, 北森, Workable, SmartRecruiters, Workday, Rippling or Personio. The assistant pastes the careers URL and gets the board's own postings with the board's own posting date and days_open where it reports one (date_signal: not_reported_by_ats otherwise), a direct apply link on the employer's own tenant and a description excerpt. No skills, no ghost_score, no relist history and no watches, because those need repeated observation, and nothing is stored. A Feishu Hire portal, SuccessFactors, LinkedIn or a job site is refused with the reason: those are access controls or other people's sites, and this index never looks for another way in. One live read per board per 15 minutes per server process; a repeat is answered from memory.

Your own employers (0.8.13). The public index is a curated list for three sectors; the employers one person targets are wider than that. add_employer(url) (CLI ohp add-employer) puts any board the adapters can read into the index on your machine and crawls it at once: from then on search_jobs(company=...), watch_intent, refresh_index and ghost_score cover it exactly like a seeded employer, and the weekly refresh keeps it. Such employers are flagged user_added, never enter the public snapshot (they say where you are looking), and remove_employer takes them out. The result also carries a prefilled GitHub issue link to propose the employer for the public index, which sends nothing unless you open it. A board can also be watched: watch_intent with board_url (plus location / title) makes check_watches read it live on every pull and report what appeared and what disappeared since the last pull.

Employers OpenHire cannot read (0.8.14). Some employers in these sectors post where no adapter can read: Feishu Recruitment (飞书招聘, a signed list API), BOSS直聘 only, or a careers page without a list. list_portals (CLI ohp portals) lists them with a link to each one's own careers portal, what a browser shows there without logging in, and the day it was last checked. The same list is a page you can click through: gzchenhao.github.io/openhire/portals.html. An employer the index does read that also posts on such a portal (星动纪元 reads through Moka and also lists roles on Feishu) carries also_posts_on in get_company_info, because the roles that are only there are missing from search results.

Asking about one employer

search_jobs(company=...) takes whatever the user actually said — an id (unitree), or any part of the name in either language (宇树, Unitree, XPeng). A name this index does not carry comes back as the empty-result object naming the company, never as an unfiltered search.

ohp search --company 宇树 --role-family engineering
ohp search --company waymo --distinct          # one row per role, not one per city

--distinct (collapse_role_group over MCP) keeps one row per role_group and adds role_group_size. About 20% of a page is the same role listed once per city; folding is opt-in because each city row has its own job_id and apply_channel, which matters under a location or visa constraint.

The five protocol fields

Every listing is valid schema.org/JobPosting, plus:

  • verified_at — last moment confirmed live on the employer's own site

  • source — employer_site | ats_public_api (never a job board)

  • ghost_score — 0–1 listing-activity signal, aged off the real posting date (lower = fresher). A noise filter, not an accusation: long-open listings are often evergreen talent pools or slow pipelines — the score simply lets agents down-rank low-activity noise

  • response_sla_days — the employer's OWN committed reply window. Null on almost every row, and that null is meaningful: it is set only when an employer claims their tenant. We never infer or estimate it, because we cannot observe a reply even in principle — the application deep-links to the employer and never touches this server. Read null as "no employer has claimed this tenant", not as "missing" or "slow". Employers: claim yours — free, verified by corporate identity, never by payment. It buys a verified badge, the ability to correct listing status (an evergreen pool carrying an unfair staleness score, say), and this field. It does not buy rank: ranking is a locked pure function of (match, freshness), and a test freezes that signature.

  • apply_channel — always the employer's own application URL, deep-linked to the specific job

Privacy Policy

Short version: there is no résumé field in the protocol, matching runs on your machine, and the only user-originated value the server ever stores is an anonymous client-generated fingerprint. No analytics, no telemetry, no third-party sharing. Full policy: docs/PRIVACY.md.

Privacy model

Résumé / PII upload

never — matching runs locally; a résumé never transits the server, and we never store one

What the server sees

one anonymous, client-generated fingerprint + hard filters

Repo scan

local-only · personal projects · explicit consent · opt-out anytime

Job sources

first-party only: employer career pages + public ATS APIs (Greenhouse / Lever / Ashby)

First-run data — the snapshot vs. fresh

ohp bootstrap (default) downloads a small public index snapshot (a GitHub Release asset — companies + jobs only, zero user data) and then runs one incremental crawl to refresh verified_at / delisting. --fresh skips the snapshot and crawls the public ATS from scratch with the free offline heuristic extractor. Either way: no account, no PII.

Two things that surprise people:

  • The incremental crawl is slow and quiet. On a cold index it can run for 20+ minutes with no output. It is working, not hung. If you only want the data, ohp serve skips it entirely — the server downloads the snapshot on first start and is answering in seconds.

  • The snapshot URL is pinned to the v0.1.0 tag on purpose. It looks stale; it is not. That asset is overwritten in place every Monday by a scheduled workflow, so the URL is a stable address for always-current data. Pinning it to the newest tag would break every client the moment a release is cut.

Three rules this project will never break

  1. Your résumé stays on your machine — it never transits the server, and we never store it.

  2. Ranking is not for sale — it is only f(match_quality, freshness), a locked pure function.

  3. Employers pay only for authorized, delivered outcomes — never for exposure. (v0.1 has no billing at all.)

These are enforced by CI (tests/test_privacy.py, tests/test_ranking.py, tests/test_snapshot.py).

Development

python -m venv .venv && . .venv/Scripts/activate   # Windows
pip install -e ".[dev]"
pytest        # privacy red lines + ranking + snapshot must be green

Set OPENHIRE_DATABASE_URL=postgresql+psycopg://… to run against Postgres instead of the default local SQLite file (~/.openhire/openhire.db).

Roadmap

  • v0.2 – v0.3 (shipped) — CN ATS adapters (北森 Beisen + Moka) · weekly auto-refreshed public snapshot · ghost_score public beta · 216 employers across US / EU / China

  • next — Employer claim + verified badges — employers can reserve their claim today via a corporate-identity GitHub issue (zero-cost now; badges + listing-status control ship next) · response-SLA enforcement (7-day auto-delist) · redacted proof-of-fit — an anonymous, candidate-authorized match summary that travels with an application (skills overlap only; identity never included, résumés still never transit the server)

  • v1.0 — Open, vendor-neutral schema extension for AI-readable job postings

FAQ

Where does the job data come from? Directly from 216 employers' own public ATS APIs (Greenhouse, Lever, Ashby, 北森 Beisen, Moka, Workable, SmartRecruiters, Workday, Rippling, Personio, Comeet, plus first-party employer career sites such as Li Auto's), the same endpoints that power their careers pages. No third-party job boards. source is always ats_public_api, and verified_at records the last time we confirmed each posting live. The public index is auto-refreshed weekly, so a fresh ohp bootstrap starts from recent data.

Why should I trust ghost_score? It's a pure, open, unpurchasable function — min(1, 0.15·relist_count + staleness) aged off the real ATS posting date, not our crawl date. The formula lives in pipeline/ghost_score.py, is unit-tested, and takes no money as input (red line #2). Long-open, repeatedly-relisted postings score higher; you can always re-rank client-side. Read it as signal-to-noise, not bad faith: plenty of high-scoring listings are legitimate evergreen talent pools. Employers who want their listing activity represented accurately can claim their tenant (see Roadmap).

Does my résumé actually go through the server — really? No. There is no résumé anywhere in the protocol. authorize_application has no résumé/file parameter (it structurally cannot accept one), matching runs on your machine, and the only thing that ever transits the server is an anonymous fingerprint the client generates, like #a3f9-k2p7-x8q1. This is enforced by tests/test_privacy.py, and the published snapshot carries zero user data (tests/test_snapshot.py).

Does it support China (中国区)? Yes — this is what sets OpenHire apart. Employers on 北森 Beisen (<tenant>.zhiye.com) and Moka (app.mokahr.com) are indexed: 20+ autonomous-driving / robotics / embodied-AI companies including 宇树 Unitree, 小鹏 XPeng, 优必选 UBTECH, 梅卡曼德 Mech-Mind, 速腾聚创 RoboSense, 元戎启行 DeepRoute, 星海图 Galaxea, 傅利叶 Fourier, 普渡 Pudu. Pay published as 月薪 keeps its real period (salary_period), so a salary floor no longer silently drops Chinese roles.

飞书招聘 (Feishu Hire) is not supported and won't be: it signs its job-list requests with a ByteDance _signature and gates them behind a captcha SDK, so its listings are not publicly readable. We don't break anti-bot measures.

How do I get a company added? Open a Company inclusion request issue (title it with the company + its ATS URL) — this is the best way to contribute. If you code, add it to src/openhire/seed/candidates.py (company slug + ATS vendor/tenant) and open a PR; the seeder validates tenants against the live API.

License

MIT © OpenHire Protocol · PRs welcome.


Built by a deep-tech headhunter who does not write code, pair-programming with Claude Code. Full acceptance reports, including the mistakes, in reports/.

Available Tools

13 tools
add_employerAdd an employer to my indexA
Idempotent

Put one employer's public job board into the index on THIS machine, and crawl it now.

The public index is a curated list (AI infra, autonomous driving, embodied AI). The employers a user actually targets are wider than that, so any board a vendor adapter can read goes into the user's own index on request: Greenhouse, Lever, Ashby, Moka, 北森, Workable, SmartRecruiters, Workday, Rippling or Personio (.wd5.myworkdayjobs.com/), any sector. After this call the employer behaves like a seeded one: search_jobs(company=...) with the board's own posting dates, days_open and ghost_score; watch_intent on it; refresh_index re-crawls it; the weekly refresh keeps it. It is flagged user_added, is never part of the public snapshot, and remove_employer takes it out.

Use it when the user names an employer this index does not carry and has its careers URL (read_board shows the board first without storing anything; add_employer is the step after, when the user wants it watched). A closed portal (Feishu Hire, SuccessFactors, Taleo, iCIMS, LinkedIn, BOSS直聘) is refused with the reason, as read_board refuses it.

Privacy: the vendor sees the same tenant read the weekly crawl makes; nothing about the user is sent anywhere, and which employers a user added stays on this machine.

Args: url: the careers or board URL as the user pasted it. name: the employer's display name (defaults to the board's tenant name).

Returns: added, company_id, jobs_indexed, count_on_board, and suggest_for_public_index, a prefilled GitHub issue link the user may open to propose the employer for everyone.

NameRequiredDescriptionDefault
urlYes
nameNo

TDQS

A4.8/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations only cover readOnly/openWorld/idempotent/destructive; the description adds substantially more: post-call behavior (search_jobs with posting dates, days_open, ghost_score; watch_intent; refresh_index; weekly refresh retention), the user_added flag, exclusion from the public snapshot, removal via remove_employer, and a privacy statement about what the vendor sees. This is exactly the behavioral context annotations cannot provide.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Front-loaded with the core action and reasonably structured, but the vendor enumeration and the parenthetical Workday URL format add length that could be trimmed; nearly every sentence still carries information an agent needs, so the verbosity is closer to dense than padded.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

With no output schema, the description supplies the return fields (added, company_id, jobs_indexed, count_on_board, suggest_for_public_index), the side effects, and the failure mode for unsupported portals. An agent has everything needed to call it correctly and set expectations.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 0%, so the description must carry parameter meaning; the Args block does document both: url as the careers/board URL 'as the user pasted it' and name as the display name defaulting to the board's tenant name. The url semantics are slightly thin (accepted formats are only implied by the vendor list), keeping it below a 5.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

Opens with a specific verb+resource+scope: 'Put one employer's public job board into the index on THIS machine, and crawl it now.' The 'THIS machine' phrasing distinguishes it from the curated public index, and the later sentence explicitly contrasts it with read_board (preview, stores nothing).

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Gives a concrete trigger condition ('when the user names an employer this index does not carry and has its careers URL'), names the alternative (read_board shows the board first without storing anything; add_employer is the step after), and states the exclusion (closed portals refused with reason). Nothing is left to inference.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

authorize_applicationAuthorize applicationA
Idempotent

Record an authorized, employer-direct application. REFUSES résumés.

Idempotent: the same fingerprint authorizing the same job again gets the original receipt back (already_authorized: true); no second record is written.

(Formerly apply — renamed to make explicit that this only records the user's authorization to apply as themselves; it never submits anything on their behalf.)

This tool never accepts a résumé, file, cover letter, name, email or phone — a résumé never transits the server. It only takes a job_id, an anonymous fingerprint, and an explicit per-job authorization. On success it returns the apply_channel (the employer's own application URL) for the user to submit as themselves, plus resume_transmitted=false. Do NOT paste résumé content into any argument.

REFUSES means refuses: any argument this tool does not declare (a resume key, a cv, a file, anything) is answered with ERR_PII_NOT_ACCEPTED and nothing is recorded. It is not silently dropped, so a client that sends one finds out.

Args: job_id: the job to apply to (from search_jobs / check_watches). fingerprint: the user's anonymous fingerprint. authorized: must be true — explicit per-job consent.

NameRequiredDescriptionDefault
job_idYes
authorizedYes
fingerprintYes

TDQS

A4.7/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already declare mutation, idempotency, and non-destructiveness, but the description adds substantial behavior beyond them: exact idempotency semantics (`already_authorized: true`, original receipt returned), refusal behavior for undeclared arguments (`ERR_PII_NOT_ACCEPTED`), and the returned `apply_channel` plus `resume_transmitted=false`. This is highly transparent.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is front-loaded with the core action and the résumé refusal, and each paragraph has a clear purpose. It is somewhat repetitive about résumé/file refusal, but that repetition is defensible for a safety-critical constraint.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

With no output schema, the description still explains key return behavior (`apply_channel`, `resume_transmitted=false`, `already_authorized: true`). It covers mutation behavior, idempotency, argument refusal, and the no-submission guarantee, leaving no significant gap for calling the tool correctly.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters5/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 0%, so the description carries the full burden. It explains all three parameters: `job_id` comes from search/watches, `fingerprint` is anonymous, and `authorized` must be true for explicit per-job consent. It also warns that any undeclared argument is refused, which is important operational parameter guidance.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description states a specific verb and resource: it records the user's authorization to apply as themselves and explicitly never submits anything. It distinguishes itself from its former name (`apply`) and clarifies the job_id source (`search_jobs` / `check_watches`), so an agent can tell what this tool does and does not do.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Usage context is clear: the tool is for recording explicit per-job authorization and returns the employer's own application URL for the user to submit. It does not explicitly compare against nearby siblings like `watch_intent` or `check_watches`, but it gives enough context for correct selection.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

check_applicationsCheck applicationsA
Idempotent

An answer to "I applied and heard nothing: are they even still hiring?".

Follows the postings this fingerprint applied to and reports, from the employer's own system, whether each is still live, has been taken down since (closed: stop waiting on it), or has not been returned by the employer's system for 30 days (not_verified_recently). Postings recorded with authorize_application are included automatically. When the user says they applied to postings found in this index (on the employer's site or anywhere else), pass those job_ids as add_job_ids to follow them; remove_job_ids stops following ones they added. Only job_ids are stored, keyed by the anonymous fingerprint: never a name, résumé or where they applied from.

Start your answer with the summary sentence, then list closed first, then not_verified_recently, then open, quoting each row's status_in_words. Do not say an employer ignored the user or that a closed posting was filled: we see the posting, never the employer's inbox. Call it again at the start of a later session to see what changed.

Args: fingerprint: the user's anonymous fingerprint (the same one their watches use). add_job_ids: job_ids (from search_jobs) the user says they applied to. remove_job_ids: job_ids to stop following.

NameRequiredDescriptionDefault
add_job_idsNo
fingerprintYes
remove_job_idsNo

TDQS

A4.7/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Goes well beyond the annotations by disclosing what is persisted (only job_ids, keyed by anonymous fingerprint, never a name or résumé), the effect of add/remove on the follow list, and an important epistemic limit — the system sees postings, never the employer's inbox, so the agent must not claim an employer ignored the user. The 30-day not_verified_recently rule is a concrete behavioral detail not available anywhere else. No rate limits or freshness beyond that are given.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Front-loaded with the user question, then scope, then parameters, then output ordering — a sensible structure. It is longer than typical and the mixed user-voice framing plus output-formatting instructions make it slightly dense, but nearly every sentence carries operational information the agent needs.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

No output schema exists, yet the description specifies the return shape: a summary sentence plus rows bucketed as closed, not_verified_recently, and open, each with status_in_words, and prescribes ordering. Combined with the storage and mutation semantics, an agent can call this and present results correctly without further information.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters5/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 0%, so the description must carry all three parameters, and it does: fingerprint is defined as the same anonymous fingerprint the user's watches use, add_job_ids as job_ids from search_jobs the user reports applying to, and remove_job_ids as un-following ones they added. That is complete coverage where the schema offers none.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The opening line frames the exact user question, then states a specific verb and resource: follows the postings a fingerprint applied to and reports whether each is still live, closed, or not_verified_recently. It is clearly distinguishable from check_watches and authorize_application, which it names directly.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Gives explicit triggers ("user says they applied to postings found in this index"), tells the agent to pass add_job_ids in that case and remove_job_ids to stop following, and says to call it again at the start of a later session. Alternatives (authorize_application, search_jobs as the job_id source) are named with the condition that links them.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

check_employerCheck an employerA
Read-onlyIdempotent

Facts about one employer, each with a source and a time. Never a score or a verdict.

Use it when the user asks "is this company real / legit / what it says it is?" or is about to apply somewhere they have never heard of. It answers with a checklist, not a judgement: a company can fail an item and be fine (a young domain, an overseas site with no ICP record) or pass every item and still waste the user's time. Relay the items; do not total them, and do not call a company "safe" or "a scam" on this basis.

What runs for free, with nothing from the user:

  • in_index: whether the employer's own system is in this index, how many postings are live, the median days open, whether the posting dates are the employer system's own or self-reported (date_source), whether the employer claimed its tenant. For an employer we know but do not index (Feishu-hosted, 蔚来, or one that has ceased operations) the item says that instead.

  • domain_age (RDAP registration date; under 180 days is reported, not judged), site_history (earliest Wayback Machine capture), icp_on_homepage (the ICP 备案号 the employer's own homepage carries, if any). These need the employer's own domain: pass domain when the employer's careers page is on a vendor host.

  • posting_red_flags: when the user pastes the posting text, the words recruitment scams rely on (境外高薪, 包机票, 不限经验, 打字员, visa provided ...) are listed if present. Absence proves nothing; presence is worth saying out loud.

  • sanctions_screen: the OFAC SDN and consolidated sanctions lists, downloaded to the user's machine at most once a day and matched against the company's name there (the name is sent nowhere). A match names the entry, list and programme; it is a reason to read the entry, not a verdict, since names collide. A live company registration proves little on its own: Prince Group's Hong Kong companies were on the register the day they were sanctioned.

  • org_events: the organisational and financing events EarlyTip (earlytip.com) has on public record for the employer (executive departures, restructurings, funding rounds, acquisitions, bankruptcies, new companies; 2026-09-20 onwards), read from its public export, downloaded whole to the user's machine once a day and matched there by the export's published company_key. Each event is a type, one date (the event's own when known, else EarlyTip's publication day: date_kind), the amount where EarlyTip's record carries one (amount, verbatim), the source domain and a plain link; never the headline, and no link where the source site's terms list linking among the prohibited acts (link_withheld). role says whether the employer is the event's subject or its counterparty; unverified carries EarlyTip's own "待核实" marker. EarlyTip checked its values against its own published headline and summary, not the source article: relay an event as "on EarlyTip's record, see the source", never as an established fact, and reason: no_record means the file holds nothing on this employer, which says nothing about it.

What costs money and is therefore pending_user unless the user set their own key:

  • registry_record: the company register (成立日期, 经营状态, 注册资本, 参保人数, 法定 代表人). The government site sits behind a captcha; 天眼查 sells it per call. With TIANYANCHA_API_KEY set on the user's machine this tool fetches it once per call, paid by the user's own 天眼查 account; the key and the lookup never reach us. Without it, pending_user[] lists the ways: set the key, use a 天眼查 MCP the assistant already has, or the free manual lookups (爱企查, 国家企业信用信息公示系统; for a Hong Kong or overseas employer the HK Companies Registry e-Services search, the HK Police Scameter for a phone number / URL / receiving account, and OpenCorporates). Offer them; do not invent the record. If the assistant already has a 天眼查 tool or connector (for example the one built into Tencent WorkBuddy), use it to fill registry_record directly instead of sending the user off to apply for a key.

An unavailable item carries reason: network means this query could not reach the source (a restricted network, a timeout) and says nothing about the employer; say that rather than "no record". no_record / http_status mean the source answered.

Privacy: this tool sends the company's name or domain to RDAP, the Internet Archive and the company's own site, and to 天眼查 only with the user's key. The OFAC lists and the EarlyTip file are downloaded whole, so neither learns the name. Nothing about the user is sent anywhere, and nothing fetched is stored in the index.

Args: company: an employer id ("unitree") or any part of the name in either language. Ambiguous input is refused with the candidates, never guessed. domain: the employer's own web domain ("unitree.com"), when the user has it or when the company is not in the index. Either argument alone is enough. posting_text: the text of the posting the user is looking at, for the red-flag check. job_id: a posting from search_jobs / check_watches. The employer is taken from it and the red-flag check runs over the posting as this index holds it, so you can check a role you just found without pasting anything. posting_text, if also given, wins. message_text: what the other side sent the user in a private message or chat (most job scams happen there, not in the posting): scanned on this machine for the usual lures (fees, loans, "add my WeChat", "install our app", screen sharing, ID photos, verification codes) and for outsourcing / labour-dispatch wording, and neither kept nor sent anywhere. Works alone, without company or domain. Before passing it, remove the user's own name, phone number and ID details from the text. Refused on the hosted endpoint (ERR_LOCAL_ONLY): a pasted chat can carry the user's personal data, which never goes through a server. Any text you pass also gets an outsourcing_words item: which outsourcing words appear, never a verdict.

Returns: checks[] (id, label, status: verified | not_verified | unavailable | not_applicable | pending_user, note, source, checked_at, plus the item's own fields), pending_user[] with how[], summary, not_a_verdict, privacy.

NameRequiredDescriptionDefault
domainNo
job_idNo
companyNo
message_textNo
posting_textNo

TDQS

A4.7/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations only say read-only/non-destructive/open-world; the description goes far beyond with cost and consent semantics (`pending_user` unless the user's own key is set), rate limits (OFAC and EarlyTip files downloaded at most once a day), a hard local-only refusal for `message_text` (ERR_LOCAL_ONLY), and a precise privacy map of what is sent to RDAP, Internet Archive, the employer's site and 天眼查. It also defines failure semantics (`unavailable` + `reason: network` vs `no_record`) so the agent does not misreport a network error as 'no record'.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Front-loaded with the one thing the agent must not get wrong (checklist, not verdict), then grouped by free vs paid vs local-only, which is genuinely useful structure. It is long and some bullet prose (e.g. the EarlyTip event-field walkthrough) is denser than an agent needs to select and call the tool, but almost every clause earns its place.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

No output schema exists, and the description still enumerates the return shape: `checks[]` with a status enum (verified | not_verified | unavailable | not_applicable | pending_user), `pending_user[]` with `how[]`, plus `summary`, `not_a_verdict` and `privacy`. Combined with per-check explanations and the refusal case, an agent has everything needed to call it and relay results correctly.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters5/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 0% — the schema is bare titles and nullable strings — so the description carries the full burden, and it does: it explains `company` (id or partial name, ambiguous input refused with candidates rather than guessed), `domain` (needed when the careers page sits on a vendor host), `job_id` (pulls the employer from a posting found via search_jobs/check_watches), and precedence ('posting_text, if also given, wins'). It even adds an operational instruction for `message_text`: strip the user's name, phone and ID before passing it.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

Opens with a specific verb-and-resource framing: facts about one employer, each with source and timestamp, explicitly 'never a score or a verdict'. That is far more precise than the title. It does not, however, distinguish itself from the sibling `get_company_info`, which an agent must choose between and which the description never mentions.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Gives an explicit trigger (the user asks 'is this real/legit' or is about to apply somewhere unknown), plus how to frame the answer (relay items, do not total, do not say 'safe' or 'a scam'). It also names alternatives to route to — an existing 天眼查 MCP/connector, or manual free lookups — and says when each applies.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

check_watchesCheck watchesA

Pull matches that are new since this fingerprint's last check.

stdio has no server push, so clients pull: call this at the start of a session. Returns the new matches per watch and advances each watch's last-notified marker. A board watch (registered with board_url) is read live here: its entry carries new_matches (appeared on the board since the last pull), closed_since_last (gone from the board) and total_matching; a board that does not answer comes back as board_error and nothing is marked closed on its account.

The FIRST pull on a watch returns everything matching it, not an increment: nothing has been reported for that watch before, so the whole standing set is new to the user. Each result says which it is via is_first_pull; do not present a first pull to the user as "postings that appeared since last time".

Each result also carries total_matching and truncated: a broad watch can match hundreds of postings and only the 100 best-ranked are returned. Tell the user when truncated is true; the rest is not re-reported later.

Start your answer with the summary sentence (top level and per watch): it already says whether this is the first-pull baseline or an increment, and how many.

NameRequiredDescriptionDefault
fingerprintYes

TDQS

A4.4/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Goes well beyond the annotations: discloses that each call advances the last-notified marker (consistent with readOnlyHint=false and idempotentHint=false), explains first-pull semantics, board_error handling, and truncation. This is exactly the stateful-read nuance the annotations cannot convey.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Front-loaded with the core action and the session-timing cue, then layered detail. It is longer than typical but each paragraph carries genuine agent-facing guidance (first pull, truncation, summary sentence); only minor tightening is possible.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

There is no output schema, so the description carries the full burden of explaining return values — and it does, naming new_matches, closed_since_last, total_matching, board_error, is_first_pull, truncated, and summary. Nothing an agent needs to interpret the response is missing.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The single fingerprint parameter has 0% schema description coverage. The description implies its role ('since this fingerprint's last check'), which is genuine added meaning, but never says where a fingerprint comes from or how to obtain it, leaving a real gap.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb and resource — pulling new matches since this fingerprint's last check — and implicitly separates itself from siblings like search_jobs and read_board by its incremental, watch-scoped nature. An agent can tell what this does without opening the schema.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Explicitly says 'call this at the start of a session' and explains why (stdio has no server push, so clients pull). It does not name the alternative tools or state when-not-to-use, but the usage context is clear enough to act on.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

get_company_infoCompany trust signalsA
Read-onlyIdempotent

Aggregate, anonymous trust signals for one employer.

Returns ghost_score_avg, active_jobs, and index_built_at (when the index was last built). NEVER returns any individual candidate data: the server holds none.

posting_dates_reported is false when no live posting of this employer carries the employer's own posting date (first-party mirrors such as Li Auto report none), and postings_without_reported_date counts those rows; their days_open, and so this employer's median_days_open and ghost_score_avg, are counted from the day this index first saw each posting, a lower bound on age rather than the employer's timeline.

active_jobs and every aggregate count only postings the employer's own system returned within the last 30 days of this index; unverified_over_30d counts the ones it still holds that went unconfirmed longer (most often a careers board that moved). Those are not shown by search_jobs. Nonzero means a gap in our reading, never a fact about the employer.

claimed is true only when the employer has claimed this tenant and we verified them by corporate identity, never by payment, and it never affects ranking. It is the only signal here that comes from the employer rather than from their public ATS data, and it is what makes response_sla_days non-null on their postings.

hiring_trend counts what the employer's own system shows: postings published in the 30 days up to the index's newest verification, the 30 days before, and postings taken down in the last 30, with in_words (zh, en) to quote. Use it when the user is about to sign or bet months on an internship and asks whether the company is growing or shrinking. Report the counts; never turn them into "they are about to lay off" or "safe to join".

Takes an id or any part of the name in either language, like search_jobs' company.

Known-but-not-indexed employers (Momenta, 小马智行 Pony.ai, 智元 AgiBot, MiniMax, 智谱 Zhipu, 商汤 SenseTime, 逐际动力 LimX, 自变量 X Square, 千寻智能 Spirit AI, 加速进化 Booster, 穹彻智能 Noematrix, 众擎 EngineAI) return a structured answer instead of ERR_COMPANY_NOT_FOUND: indexed: false, careers_url (their own portal), reason (their careers site runs on Feishu Recruitment, whose job-list API requires a request signature; we treat that as access control and do not work around it) and employer_opt_in (the employer can authorize the read-only Feishu open-platform scopes hire:site:readonly and hire:site_job_post:readonly). 蔚来 NIO gets the same shape with its own reason: its careers page's security policy blocks this crawler (HTTP 567), which we do not work around. There are no trust signals in that answer because we hold none of their postings; do not read the absence as a verdict on the employer.

NameRequiredDescriptionDefault
company_idYes

TDQS

A4.7/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already declare read-only, idempotent, non-destructive, closed-world behavior, but the description adds substantial context beyond that: it explains that no individual candidate data is returned, defines lower-bound age semantics for postings without reported dates, explains what unverified_over_30d means, clarifies that claimed is employer-provided and never affects ranking, and describes the structured fallback response for known-but-not-indexed employers.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is front-loaded with purpose and return fields, and each paragraph explains a distinct signal or caveat. It is still quite long for a one-parameter tool, especially the enumerated list of known-but-not-indexed employers, but most of the length earns its place by preventing misinterpretation.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given that there is no output schema and the input schema has no parameter descriptions, the description carries the full burden and does so thoroughly: it explains the returned fields, the special fallback shape, the meaning of key flags, and how to interpret the counts without overclaiming.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters5/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 0% and the single company_id parameter is undocumented in the schema. The description compensates fully by stating that it takes an id or any part of the name in either language, like search_jobs' company parameter.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The opening sentence states a specific resource and operation: aggregate, anonymous trust signals for one employer. It also explicitly contrasts with search_jobs regarding unverified_over_30d, so the agent can distinguish it from a sibling without opening either schema.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

It gives explicit when-to-use guidance for hiring_trend (when the user is about to sign or bet months on an internship and asks whether the company is growing or shrinking), and it clarifies that certain counts are not shown by search_jobs. It does not give a full routing rule for the whole tool versus check_employer or other siblings, but the context is clear.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

list_portalsCareers portals OpenHire cannot readA
Read-onlyIdempotent

The blind spots as a link directory: employers in AI infra, autonomous driving and embodied AI whose postings this index cannot read, each with its own careers portal.

Most are Chinese employers on Feishu Recruitment (飞书招聘), whose job-list API needs a request signature this index treats as access control; a few publish only on BOSS直聘 or keep a careers page without a readable list. Their postings are public to a person in a browser; OpenHire cannot index, date or watch them.

Use it when the user asks about these sectors broadly (具身智能有哪些公司在招, autonomous driving roles in Shanghai), after the search_jobs results: say which employers post elsewhere and give the links, so a blind spot is one click away instead of invisible. If you have a browser tool, offer to open a portal. Never present these links as OpenHire postings and never invent posting dates for them.

Args: sector: autonomous_driving, embodied_ai or ai_infra (自动驾驶, 具身, 大模型 work too); leave it out for all three.

Returns: PortalDirectory with portals[] (name, careers_url, what a browser shows without logging in, why the employer is not indexed, last_checked) and a note on what OpenHire does not do with them.

NameRequiredDescriptionDefault
sectorNo

TDQS

A4.9/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Beyond the read-only, idempotent, non-destructive annotations, the description discloses why these employers are not indexed, that postings are public in a browser but cannot be indexed/dated/watched by OpenHire, and that posting dates must never be invented. This is rich behavioral context for interpreting and presenting results.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is front-loaded with the purpose, then provides usage context, arguments, and return shape in a clear order. It is longer than minimal, but nearly every sentence adds actionable context about blind spots, sectors, and presentation constraints.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Despite having no output schema, the description describes the PortalDirectory return shape in useful detail: portals with name, careers_url, browser-visible content, reason for not being indexed, last_checked, and a note. It also covers the behavioral and usage caveats an agent needs before calling and presenting results.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters5/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 0%, but the description fully explains the sole sector parameter: accepted values autonomous_driving, embodied_ai, and ai_infra, including Chinese equivalents, and that leaving it out returns all three. This compensates completely for the missing schema documentation.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description states a specific resource and scope: a link directory of employers in AI infra, autonomous driving, and embodied AI whose postings OpenHire cannot read, each with a careers portal. It clearly differentiates itself from search_jobs by framing these as blind spots that are not indexed.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

It explicitly says to use this when the user asks about these sectors broadly, gives concrete query examples, says to use it after search_jobs results, and describes how to offer opening a portal if a browser tool exists. It also includes output-use constraints such as not presenting the links as OpenHire postings.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

read_boardRead an employer's boardA
Read-onlyIdempotent

Read one employer's public job board live, whether or not the employer is in this index.

Use it when the user names an employer this index does not carry (search_jobs returned the empty-result object, or the employer is outside AI infra, autonomous driving and embodied AI) and has, or can find on the employer's careers page, a board URL on a vendor this index knows how to read: Greenhouse (job-boards.greenhouse.io/), Lever (jobs.lever.co/), Ashby (jobs.ashbyhq.com/), Moka (app.mokahr.com/apply//), 北森 (.zhiye.com), Workable (apply.workable.com/), SmartRecruiters (jobs.smartrecruiters.com/) Workday (.wd5.myworkdayjobs.com/), Rippling (ats.rippling.com//jobs) or Personio (.jobs.personio.de). Any sector. To keep the employer, add_employer puts the same board into the user's own index.

What you get: the board's own postings with title, location, the board's own posting date and days_open where it reports one (date_signal: not_reported_by_ats otherwise), a direct apply link on the employer's own tenant, and a description excerpt that starts where the role does (company boilerplate skipped). Pass ats_job_id from a previous read to get that one posting with its whole description, when the user wants the job description itself read or summarised. What you do not get: skills, ghost_score, relist history or watches, which need repeated observation; and nothing is stored. If the employer turns out to be in the index the result says so and search_jobs is the richer source.

Refused with ERR_UNSUPPORTED_BOARD, with the reason: a Feishu Hire / Lark portal (request signatures, an access control this index never bypasses), SuccessFactors, Taleo, iCIMS, LinkedIn, JobsDB or BOSS直聘. Tell the user to open that page themselves; never look for another way in.

Politeness: one live read per board per 15 minutes per server process (a repeat within that window is answered from memory, from_cache: true), with the same request headers as the weekly crawl. A board that does not answer is reported as such, never as a fact about the employer.

Privacy: sends only the board's tenant to the vendor's public endpoint. Nothing about the user is sent anywhere, and nothing read is stored.

Args: url: the careers or board URL as the user pasted it. location: only postings whose location text contains this, in either language (香港 = Hong Kong = HK, 广州 reaches 天河区). title: words in the job title, any match ("hr" reaches HR, HRBP, recruiter, talent acquisition); the filter for roles no skill tag names. limit / offset: page through total_matching (newest posting date first, undated last). ats_job_id: one posting's id from an earlier read; returns it alone with the full text.

NameRequiredDescriptionDefault
urlYes
limitNo
titleNo
offsetNo
locationNo
ats_job_idNo

TDQS

A4.9/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already declare readOnly/openWorld/idempotent/non-destructive, yet the description still adds material facts the annotations cannot convey: a 15-minute-per-board read limit with cached repeats reported as from_cache, the same headers as the weekly crawl, an explicit ERR_UNSUPPORTED_BOARD refusal list (Feishu Hire, SuccessFactors, Taleo, iCIMS, LinkedIn, JobsDB, BOSS直聘), and a 'never look for another way in' instruction. It also discloses what is absent (skills, ghost_score, relist history, watches) and that nothing is stored.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Well front-loaded: purpose, then when-to-use, then what you get / do not get, then refusals, rate limits, privacy, and args. Dense but mostly earned. The only waste is minor repetition ('nothing is stored' appears in both the 'what you do not get' block and the privacy block), and the vendor URL list is long though functionally necessary.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

With no output schema, six parameters, and 0% schema coverage, the description fills every gap an agent needs: return shape (title, location, posting date, days_open or date_signal: not_reported_by_ats, direct apply link, description excerpt), pagination semantics, cache behavior, error taxonomy, and privacy boundaries. Nothing material is left for the agent to infer before calling.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters5/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 0%, so the description carries the full burden and does: url is 'the careers or board URL as the user pasted it'; location is a substring match with cross-language examples (香港 = Hong Kong = HK, 广州 reaches 天河区); title is any-word matching with 'hr' reaching HR/HRBP/recruiter/talent acquisition; limit/offset page through total_matching with a stated sort order (newest posting date first, undated last); ats_job_id returns a single posting with the full description. Every parameter is given behavior the bare schema cannot express.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

Opens with a specific verb and resource ('Read one employer's public job board live') and immediately scopes it ('whether or not the employer is in this index'). That single clause separates it from search_jobs, which only covers the indexed employers, so an agent can route correctly without opening either schema.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Gives an explicit trigger (search_jobs returned the empty-result object, or the employer is outside AI infra / autonomous driving / embodied AI) plus the prerequisite (a board URL on a named vendor). It names the hand-off to add_employer to keep the employer and redirects to search_jobs when the employer turns out to be indexed, so both branches are covered.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

refresh_indexRefresh one employerA

Re-crawl ONE employer's public ATS now. Takes about a minute. Throttled to 6h.

The index is refreshed weekly, so search_jobs can be up to seven days behind and check_watches has nothing new to report until it moves. This is the manual nudge for the case that matters: the user is about to act on one employer and wants today's truth.

Rules worth knowing before you call it:

  • ONE employer per call. A full crawl is 20+ minutes and no client will wait; a vague word like "robot" is refused with the list of candidates rather than fanned out into eleven live crawls.

  • At most one crawl per employer per 6 hours. A throttled call returns immediately with refreshed: false, reason: "throttled" and last_refreshed_at — no network request is made. That is not an error: it means the data you already hold is that fresh.

  • An employer we know but deliberately do not index (the Feishu-hosted ones, 蔚来 NIO, the ones whose own site has no readable job list, and 毫末智行 which has ceased operations; see search_jobs) returns reason: "known_not_indexed" with the same portal, reason and employer_opt_in the other tools give, never unknown_company.

  • Do NOT call this speculatively or in a loop. Every call hits somebody else's public endpoint. Search first; refresh only when the user needs today's state of one employer.

Args: company: one employer — an id ("unitree") or any part of the name in either language ("宇树", "XPeng"). Ambiguous input is refused, not guessed.

Returns: refreshed plus last_refreshed_at / next_allowed_at; when it did run, also jobs_new / jobs_updated / jobs_delisted / jobs_unchanged, delisted (job_id and title of the rows this crawl took down, first 20) and counts_note on what the counts mean.

NameRequiredDescriptionDefault
companyYes

TDQS

A4.9/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Far beyond the annotations, it discloses the 6-hour throttle, that a throttled call makes zero network requests and returns refreshed:false rather than an error, the one-employer-per-call restriction with the reason (20+ minute full crawl), and the known_not_indexed branch. Annotations only cover the safety profile; this adds the real operational traits.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Front-loaded summary line with timing and throttle before any detail, then a bulleted rules section that is easy to scan. It runs long for a one-parameter tool and the known_not_indexed bullet enumerates specific employer names that add bulk, though the content is genuinely load-bearing.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

There is no output schema, and the description fully documents the return shape (refreshed, last_refreshed_at, next_allowed_at, jobs_new/updated/delisted/unchanged, delisted rows, counts_note). Combined with the throttle and error-branch coverage, an agent has everything needed to call this correctly.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters5/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 0% (bare string, no description), so the description carries the full burden and does: id form ("unitree"), partial-name form in either language ("宇树", "XPeng"), and the failure mode of ambiguous input being refused rather than guessed. This is exactly the compensation the gap requires.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description opens with a precise verb, resource, and scope: "Re-crawl ONE employer's public ATS now." The capitalized ONE and the explicit contrast with search_jobs/check_watches make it trivially distinguishable from every sibling tool.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

It states exactly when to use it ("the user is about to act on one employer and wants today's truth"), when not to ("Do NOT call this speculatively or in a loop... Search first"), and names the alternative tool. Explicit exclusions and prerequisites are both present.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

remove_employerRemove an employer I addedA
DestructiveIdempotent

Take an employer the user added (add_employer) and its postings out of this machine's index. Employers in the public index cannot be removed this way (ERR_NOT_USER_ADDED).

Args: company: the company_id add_employer returned, or any part of the employer's name.

NameRequiredDescriptionDefault
companyYes

TDQS

A4.4/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already declare destructiveHint=true, idempotentHint=true, readOnlyHint=false, so safety is covered. The description adds real behavioral context beyond them: the deletion cascades to postings, removal is confined to the local index, and it enumerates the failure mode (ERR_NOT_USER_ADDED). Return format isn't described, but that is minor here.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Front-loaded purpose sentence followed by a compact Args block that only documents the one parameter. No filler; the error code is embedded usefully rather than padded.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a single-parameter destructive tool with an existing annotation set and no output schema, the description covers scope, cascade effects, and the failure case well enough to call correctly. The only minor gap is what happens on success (confirmation vs. silent).

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters5/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 0% (the parameter is a bare 'string' titled Company), so the description carries the full load and does so: it accepts the company_id returned by add_employer or any part of the employer's name, clarifying both the value source and fuzzy-match behavior.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb (remove) plus resource (an employer and its postings) and narrows scope to 'this machine's index' versus the public index. The reference to add_employer and the ERR_NOT_USER_ADDED condition makes the boundary unambiguous relative to the closest sibling.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Gives an explicit when-not: employers in the public index cannot be removed this way, with the error code. It names the counterpart tool add_employer implicitly by scoping to user-added employers, though it doesn't route to other alternatives for the public-index case.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

salary_benchmarkSalary benchmarkA
Read-onlyIdempotent

What employers themselves publish as pay for comparable live postings.

Only ranges employers put on their own postings (most US postings carry one by law; most Chinese ones carry none). Never mixes currencies; annualises monthly pay. Low and high ends are reported separately as p25 / median / p75 (annual_low, annual_high), with postings, employers and wide_ranges (ranges spanning 2x or more). Under 20 postings there are no quantiles, only the count. Group by the level the title names (seniority, as in search_jobs) and the years the posting asks for (years_min / years_max, from the posting's own words), never by company grade.

Quote in_words. Say these are published ranges, not offers or what anyone was paid, and never tell the user a posting "pays below market, skip it": give the numbers.

NameRequiredDescriptionDefault
titleNo
sectorNo
currencyYes
locationNo
seniorityNo
years_maxNo
years_minNo
role_familyNo

TDQS

A3.9/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Beyond the read-only annotations, the description discloses important behavior: currency is never mixed, monthly pay is annualized, quantiles are suppressed under 20 postings, and wide_ranges captures ranges spanning 2x or more. It also frames the data as published ranges rather than offers or actual pay, which is critical for correct interpretation.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is dense but front-loaded, starting with the data source and then moving through output semantics and usage constraints. Most sentences earn their place, though the parenthetical about US/Chinese postings is more explanatory than necessary for invocation.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

With no output schema, the description does a good job explaining return semantics, quantile behavior, sample-size thresholds, and interpretive caveats. It is less complete on input parameter meanings, especially sector, location, role_family, and title, but the overall behavioral picture is strong enough for an agent to use the tool carefully.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters2/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 0% across 8 parameters, so the description must carry the burden. It explains seniority and years_min/years_max semantics and currency behavior, but leaves title, sector, location, and role_family largely or entirely undefined, which is a significant gap for an 8-parameter tool.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the resource: employer-published pay ranges from comparable live postings, and names the key outputs like annual_low, annual_high, p25/median/p75, postings, employers, and wide_ranges. It does not explicitly distinguish this tool from all siblings, though it references search_jobs for seniority grouping.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

It gives clear usage rules: group by seniority as named in the title and by years_min/years_max from the posting, never by company grade. It also instructs how to present results (quote in_words, call them published ranges, do not advise skipping postings), but it does not explicitly state when to choose this tool over a sibling.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

search_jobsSearch jobsA
Read-onlyIdempotent

Search the live job index by hard filters; returns ranked JobPosting[].

The server does ONLY a hard filter plus a fixed ranking of match-quality × freshness — precise re-ranking is left to you, the client, which holds the user's context. Every result includes the five protocol fields (verified_at, source, ghost_score, response_sla_days, apply_channel) plus datePosted, days_open, remote_scope, eligible_regions, role_group and ghost_reason.

Four fields are written for the person reading your answer (0.8.30): seniority is the level the title names (intern | junior | senior | staff | lead | manager | director | unknown; a product / project / account manager is an individual contributor, so it is never "manager"; the seniority filter's alias ic means every individual-contributor level (intern, junior, senior, staff, and unknown, because most titles name no level), so for a new graduate pass ["intern", "junior", "unknown"] and for an experienced IC ["senior", "staff", "unknown"]; listing levels leaves unknown out unless you name it. If you do not yet know whether the person leads people and how many years they have, ask before the first search rather than guessing the filter); date_source says whose date datePosted is (employer_ats | employer_self_reported | index_first_seen); dates_in_words is one sentence per language ("雇主发布日期 2026-09-23,在架 35 天;最近核验 2026-10-26…") to show verbatim instead of field names; check_before_apply lists what to confirm before applying, each item from a measured fact (no employer date, open over 90 days, no stated salary, remote scope unclear, unrecognised apply host, employer says closed), with an empty list meaning nothing stood out, not that the posting is vetted. Show at most 5 postings first, as six items each (title; company and location; salary, saying when none is stated; why it matches; what to confirm; apply link), then offer more.

An empty result carries status and a fixed bilingual headline saying which kind of nothing it is: index_empty, known_not_indexed, unknown_company, unknown_tag, unknown_location (no live posting's location matches the place: try the other language, a city, or China / Bay Area) or no_match; filters_applied lists every filter you sent; index_status: "preparing" means the index is still downloading on first start, so say that and retry in a minute rather than reporting a dry market.

verified_at and ghost_score answer DIFFERENT questions and routinely disagree: a posting confirmed live today can score 1.0. Live means the employer's ATS still returns it; the score means it has been returned for a long time, or keeps being relisted. ghost_reason says which input drove the score ("age only: open 367d, never relisted") so you can tell the user that instead of a bare number. Neither field measures intent — a long-open role can equally mean hard-to-fill.

How to say it: ghost_score is a measurement of time on the market, never a verdict that a posting is fake. Do not call a posting a ghost, zombie, fake job or 僵尸岗 on the strength of it, and do not tell the user "don't apply" because of it. Report what was measured — "open 1,616 days, never relisted, the ATS reports no last-touched date" — and let the user weigh it. A 1.0 can be an abandoned req or a role that has been genuinely hard to fill for four years; the number cannot tell those apart.

response_sla_days is null on almost every row, and that is a meaningful null: it is the employer's OWN committed reply window, set only when they claim their tenant. We never infer or estimate it — the application deep-links to the employer and never touches this server, so we cannot observe a reply even in principle. Read null as "no employer has claimed this tenant", never as "missing data" or "slow".

employer_correction appears only when the employer has claimed this tenant and said something about this specific role. It is the employer's own account, verified by corporate identity, and it sits BESIDE ghost_score, never on it: status: "evergreen" explains a high score (they hire continuously, so there is no single opening to fill), it does not lower it. status: "closed" means they say they are no longer hiring even though their ATS still returns the row, so stop sending people there. date_semantics: "requisition_created" means their ATS reports the day the req was opened internally rather than the day it went live, so days_open overstates for that employer. Treat all of it as the employer's claim, attributed, not as our measurement.

days_since_update is the third date and the one that usually settles it: the employer's own last-touched timestamp from their ATS. It means the record was saved, not that the content changed or that someone is reviewing applicants: say "the employer's system shows it was last updated N days ago", never "someone is actively managing it". Two rows can both score 1.0 and mean opposite things — open 367d and untouched for 367d reads as abandoned; open 327d but touched 13 days ago reads as a tended evergreen req. Among rows where the ATS reports one at all, the share of ghost>=0.99 postings touched by the employer inside 30 days has ranged from about a third to three quarters across refreshes; read the current value from pct_ghost_hi_touched_within_30d in docs/numbers.json rather than quoting a figure.

Null is NOT "abandoned": Ashby, Lever and Beisen do not report a last-touched date, and those rows carry update_signal: "not_reported_by_ats" instead. A row carrying date_signal: "not_reported_by_ats" goes one step further: its source reports no posting date at all (Li Auto's first-party mirror is one), so its datePosted and days_open are counted from the day this index first saw it, not from the employer's own date. Read those as a lower bound on age, never as the employer's timeline. Treating that null as "nobody has touched this in a year" would describe the vendor's API, not the employer. Honest limit even when present: an ATS bumps it on any edit or re-publish, so it means "touched", not necessarily "content changed".

To answer "what is hiring?", pass company — do not filter client-side.

Skills are alias-aware on the request side: 感知 finds rows tagged perception, 占用网络 finds occ / occupancy / occupancy networks. If a requested tag matches NOTHING in the index, the response switches to {results, unknown_skills, suggestions} so you can tell the user which word was dropped instead of presenting a half-match as a match.

Live means the employer's own system returned the posting within the last 30 days of this index (counted back from its newest verification); older rows are not returned. An employer whose rows are all older answers with status "not_verified_recently".

role_family filters exclude rows KNOWN to be another family; rows not yet classified (role_family null, typically the newest postings) are included, not hidden. remote_scope can also be "unknown": remote with a location we could not read; it is never reported as "worldwide" any more.

Two things worth knowing before you spend your budget:

  • role_group is shared by the same role posted in several cities — one employer may list one job 22 times, once per location. Those rows are genuinely distinct (each has its own job_id and apply_channel, which matters when the user has a location or visa constraint), but if you only need distinct opportunities, group by role_group and keep one per group. Measured, about 20% of a page is same-role repeats.

  • offset pages through the ranked list. After collapsing by role_group, call again with offset += limit to get more distinct roles. Fewer rows than limit means you reached the end. There is no server-side cursor to keep alive.

  • One call returns at most 100 rows. Ask for more and you get an object with results and truncated, not a silent first page: 100 of 198 looked exactly like "this employer has 100 jobs". truncated: true means a full page came back and there may be more, with the offset to call next in hint; truncated: false means fewer rows than the page size matched and there is no next page. For one employer, get_company_info's active_jobs is the true total.

  • Skill tags match separator-insensitively: "computer vision", "computer-vision" and "computer_vision" are one skill. The extractor emits all three spellings, so an exact-string search reached as little as 42% of the rows that had the skill.

  • An empty search does NOT return a bare list. It returns an object with results: [] plus hint, unknown_skills and suggestions, because [] alone cannot tell you whether you mistyped a tag or the market is genuinely dry. Read unknown_skills: if it is non-empty those tags exist nowhere in the index and you should retry with a suggestion; if it is empty your tags were fine and you should loosen a filter.

Args: skills: skill tags, ANY-overlap match (union), e.g. ["rust", "k8s"]. required_skills: skills that must ALL be present (AND), e.g. ["rust"]. remote: if true, only fully-remote roles. remote_scope: filter remote roles by reach: "worldwide" | "unknown" | "region_locked" | "country_locked". min_salary: salary floor. By default roles with NO stated pay are KEPT (they can't be ruled out); set require_stated_salary=true to drop them. currency: restrict to a stated-pay currency, e.g. "USD" (implies stated pay). require_stated_salary: if true, drop roles that publish no salary. role_family: coarse family filter, one of engineering | data | product | design | marketing | sales | hr | ops | other. "hr" is recruiting, talent acquisition, sourcing, HRBP, people operations, compensation and benefits, L&D (招聘、HRBP、 人力资源); "recruiting", "people" and "招聘" resolve to it. Any other value is refused (ERR_UNKNOWN_ROLE_FAMILY) rather than ignored: an unknown value used to pass through and return the whole index. Populated for most live rows, so this is an effective way to keep sales / solutions-architect roles out of an engineering search. title: caseless substrings over the job title, ANY-of, e.g. ["recruit", "招聘"] or ["感知"]. This is the filter for roles no skill tag or family can isolate: finance, legal, a specific team name; for HR it is a wider net than role_family="hr". An ASCII term must start a word ("hr" reaches HR, HRBP and HR Business Partner but not Chrome); a CJK term is a plain substring. One synonym group is expanded for you: any of hr / hrbp / human resources / recruit / talent acquisition / sourcer / people ops / 人力 / 人事 / 招聘 reaches all the others, so title=["招聘"] also finds an English "Senior Technical Recruiter". Combine with company to ask "does have any HR openings?" instead of paging their whole list. seniority: keep only rows whose title names one of these levels, ANY-of: intern, junior, senior, staff, lead, manager, director, unknown; plus the aliases "ic" (intern, junior, senior, staff, unknown: everyone who leads no team, including titles that name no level, which cannot be ruled out) and "management" (lead, manager, director). Ask the user once whether they want an individual-contributor or a management role and roughly how many years they have, then pass it: an engineer searching Zoox for perception otherwise gets "Senior Manager, Perception Data" first because it is the newest. A hard filter, never a ranking input; an unknown value is refused (ERR_UNKNOWN_SENIORITY). sector: only employers in one sector: autonomous_driving (自动驾驶 / 智驾, including the lidar, chip and truck companies built for it), embodied_ai (具身 / 机器人), ai_infra (大模型 / AI labs, chips, AI tools) or other. Use it when the user says "only robotics companies" instead of naming employers one by one. Every row carries its sector; a board the user added themselves has none and is left out. Unknown words are refused (ERR_UNKNOWN_SECTOR). entry_level: true keeps only postings open to a new graduate (应届生 / new grad): the text says campus hiring, internship or no experience required, or the title names an intern or junior level. Postings that say nothing either way are left out. Every row carries requirements {min_degree, min_years, hire_track, entry_level, visa_sponsorship, visa_quote}, read from the posting's own words with free rules (visa_sponsorship is offered / not_offered / null and visa_quote is the posting's own sentence; check_before_apply repeats it): min_degree is the lowest degree it REQUIRES (a degree it only prefers does not count), and null means the posting did not say, never "not required". Quote it as what the posting says, not as a rule the employer will enforce. posted_within_days: only postings the employer published in the last N days (1 to 365), counted from the employer's own posting date, or from the day this index first saw a row whose system gives none. Use it when the user wants fresh postings only ("只看两周内发的"); it is a filter, the ranking is unchanged. collapse_role_group: keep one row per role_group instead of one per city, and add role_group_size saying how many postings that row stands for. Cheaper when the user wants distinct opportunities; leave it false when location or visa matters, because each city row has its own job_id and apply_channel. company: restrict to one employer. Pass whatever the user said — an id ("unitree"), or any part of the name in either language ("宇树", "Unitree", "XPeng"). Exact id/name hits win; otherwise it is a caseless substring, so a broad word can match several employers. A name this index does not carry comes back as the empty-result object with unknown_companies and suggestions. Some employers are known but deliberately NOT indexed: their careers sites run on Feishu Recruitment, whose job-list API requires a request signature; we treat that as access control and do not work around it. For those (Momenta, 小马智行 Pony.ai, 智元 AgiBot, MiniMax, 智谱 Zhipu, 商汤 SenseTime, 逐际动力 LimX, 自变量 X Square, 千寻智能 Spirit AI, 加速进化 Booster, 穹彻智能 Noematrix, 众擎 EngineAI) the empty-result object carries known_not_indexed with the employer's own careers portal URL, the reason, and employer_opt_in (the employer can authorize the read-only Feishu open-platform scopes hire:site:readonly and hire:site_job_post:readonly). 蔚来 NIO is on the same list for a different reason: its own careers page publishes the postings, but its edge security policy blocks this crawler (HTTP 567) and we do not work around security controls; the employer can allowlist the crawler or authorize the same read-only scopes. Send the user to that portal; do not retry with a looser filter, and do not present the absence as "not hiring". A few more are on the same list because their own site has no readable job list (云深处 DeepRobotics publishes only on BOSS直聘; 智平方, 轻舟智航 QCraft, 鉴智 PhiGent and 松延动力 Noetix keep a page with no list, a form, a JavaScript shell or a robots-disallowed API), and 毫末智行 HAOMO has ceased operations: for it, tell the user that and send them nowhere. location: caseless substring over the employer's location text, either language ("北京", "Beijing", "Mountain View", "Remote"). Alias-aware for the cities Chinese employers spell several ways: 广州 / Guangzhou also reaches rows that name only a district ("广东·天河区", 番禺区, 黄埔区, 南沙区, 海珠区, 越秀区, 白云区), 深圳 / Shenzhen reaches 南山区, 福田区, 龙岗区, 宝安区, Beijing reaches 北京市, and "remote" reaches 远程; 上海, 杭州, 苏州, 南京, 武汉, 成都 and 合肥 match their pinyin too. Combine with remote_scope to keep or drop a country. No location filter means all locations. limit: max results (default 20); must be >= 1 (else ERR_BAD_PAGE). A negative offset clamps to 0.

Salary fields are null wherever the employer's ATS publishes no range, which is most postings outside US states with pay-transparency law; require_stated_salary and currency therefore narrow mostly to US rows, and min_salary alone KEEPS unstated rows (it cannot rule them out). Pass currency to compare in one currency; without it, stated pay is compared as an annualised number in whatever currency it was stated.

NameRequiredDescriptionDefault
limitNo
titleNo
offsetNo
remoteNo
sectorNo
skillsNo
companyNo
currencyNo
locationNo
seniorityNo
min_salaryNo
entry_levelNo
role_familyNo
remote_scopeNo
required_skillsNo
posted_within_daysNo
collapse_role_groupNo
require_stated_salaryNo

Output Schema

NameRequiredDescription
resultYes

TDQS

A4.8/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With readOnly/idempotent/openWorld=false already declared, the description still adds substantial behavioral context: empty results return an object with status/headline/filters_applied, index_status 'preparing' means retry, truncated:true vs false semantics, the 100-row cap, no server-side cursor, and the meaning of null response_sla_days and null update_signal (not_reported_by_ats). It even discloses an honest limitation (ATS bumps last-touched on any edit) that the annotations cannot convey.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness3/5

Is the description appropriately sized, front-loaded, and free of redundancy?

It is front-loaded with purpose and the args section is well organized, but the body is an extremely long wall of prose that interleaves filter semantics, data-provenance caveats, and answer-presentation instructions ('How to say it', 'Show at most 5 postings first, as six items each'). Much of it earns its place, but the volume and mixed concerns (tool contract vs. output phrasing style guide) make it hard to scan, and a large share is arguably better placed in a prompt or docs.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given 18 optional parameters, an output schema, and a tool whose results are semantically treacherous (ghost_score, null SLAs, stale dates), the description leaves nothing an agent needs unresolved: it covers filter interaction, pagination, truncation, empty-result shapes, and how to interpret each protocol field without contradicting the output schema.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters5/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 0% across 18 parameters, so the description carries the full burden — and it does: skills are ANY-overlap vs required_skills AND, min_salary keeps unstated pay unless require_stated_salary, role_family enumerates values and refuses unknown ones (ERR_UNKNOWN_ROLE_FAMILY), title's ASCII word-boundary vs CJK substring rule, seniority aliases ic/management, company accepts id or partial name in either language, location alias expansion to districts. This is exactly the compensating detail a 0%-coverage schema needs.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

Opens with a specific verb+resource+scope: 'Search the live job index by hard filters; returns ranked JobPosting[]', and immediately states what the server does and does NOT do (hard filter + fixed ranking, re-ranking left to client). It also explicitly routes to a sibling ('For one employer, get_company_info's active_jobs is the true total'), so an agent can distinguish it from get_company_info, salary_benchmark and check_employer.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Explicit routing rules throughout: 'To answer "what is <employer> hiring?", pass company — do not filter client-side', use role_family to keep sales roles out of engineering searches, use title for roles no tag can isolate, use collapse_role_group only when location/visa doesn't matter. It also gives when-NOT-to guidance (do not retry with a looser filter for known_not_indexed employers) and tells the agent to ask the user about seniority before the first search.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

watch_intentWatch a job intentA

Register a standing intent so new matches can be pulled later.

The caller supplies its OWN anonymous fingerprint (e.g. "#a3f9-k2p7-x8q1"; make it 12+ random characters, a four-character tag collides with strangers) — the client generates and owns it; the server stores but can never recover it, so persist it client-side and pass the identical one to check_watches. Only the fingerprint and non-PII filter keys are stored — never a name, email, phone or résumé. Accepted filter keys mirror search_jobs: skills (ANY-overlap), required_skills (ALL/AND — use this to keep sales / solutions-architect roles out), remote (bool), role_family (e.g. "engineering"), min_salary (int), company (one employer, resolved at registration), location (substring of the location text, alias-aware like search_jobs: 广州 also reaches 广东·天河区 rows, Beijing reaches 北京市, "remote" reaches 远程), title (list of title substrings, ANY-of, synonym-expanded for HR terms like search_jobs — the way to watch for recruiting or other roles no skill tag names), seniority (list of levels or the aliases "ic" / "management", as in search_jobs, so a watch for an engineer never fires on a Senior Manager), sector (autonomous_driving / embodied_ai / ai_infra / other, as in search_jobs: one watch for "robotics companies in Shenzhen" instead of one per employer), entry_level (true: only postings open to a new graduate, as in search_jobs), posted_within_days (only postings published in the last N days, as in search_jobs). Any other key is REFUSED (ERR_UNKNOWN_FILTER) rather than silently dropped, and a role_family outside engineering | data | product | design | marketing | sales | hr | ops | other is refused too (ERR_UNKNOWN_ROLE_FAMILY); "recruiting" and "招聘" resolve to hr.

min_salary keeps rows with NO stated pay (they cannot be ruled out); it only drops rows whose stated pay is below the floor. Pay is stated mostly where law requires it (US postings on Greenhouse/Lever/Ashby), so a watch that needs a number will lean US.

A board watch: pass board_url (a Greenhouse / Lever / Ashby / Moka / 北森 / Workable / SmartRecruiters / Workday / Rippling / Personio careers URL, as for read_board) with optional location and title and nothing else; check_watches then reads that board live on every pull and reports what appeared and what disappeared since the last pull. This is how an employer outside the index is watched. Not combinable with company, skills, remote, min_salary or role_family; a closed portal (Feishu, a job site) is refused with the reason.

Returns { watch_id, status, fingerprint, matching_now, what_happens_next, existing_watches, fingerprint_notice }. matching_now is how many postings match today (all already live, none new; null for a board watch); what_happens_next is the sentence to relay: OpenHire does not push, the user asks you to check, the first check is a baseline, a scheduled reminder is a recurring task in the assistant. existing_watches > 0 means this fingerprint was already in use; if those watches are not yours, pick a longer random fingerprint.

NameRequiredDescriptionDefault
filtersYes
fingerprintYes

TDQS

A4.9/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Beyond the annotations, it discloses that the server stores the fingerprint but can never recover it, that only non-PII filter keys are stored, and that unknown keys are refused rather than silently dropped. It also explains board-watch behavior, refusal of closed portals, and the return contract, which is substantial behavioral context for a non-read-only tool. No annotation contradiction is present.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is front-loaded with purpose and core usage, and its sections are logically ordered. However, it is quite dense and repeats 'as in search_jobs' and 'as for read_board' across many filter explanations, so it is informative but not maximally concise.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the nested filters object, zero schema descriptions, and no output schema, the description is complete: it explains filter semantics, board-watch constraints, refusal behavior, return fields, and what the user should be told next. An agent has enough information to invoke and explain the tool correctly.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters5/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 0%, so the description carries the full burden and does so thoroughly. It explains the fingerprint format, ownership, persistence requirement, collision risk, and reuse semantics, and it enumerates accepted filter keys with detailed meanings and edge cases such as min_salary behavior and board_url restrictions.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description starts with a precise verb and resource: registering a standing intent so new matches can be pulled later. It also distinguishes the tool from check_watches by explaining that the same fingerprint is later passed to that sibling. An agent can identify what the tool does without opening the schema.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description gives explicit when-to-use guidance, including how to create a normal filter watch versus a board watch via board_url, and what is not combinable with board_url. It names check_watches as the follow-up and explains what existing_watches > 0 means when the fingerprint is already in use.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

  1. 5 tool updatesv0.8.41
    • Addedcheck_applications
    • Changedcheck_employer1 field changed
      • addedInput schema / properties / message_text
        Added value: +{
        +  "anyOf": [
        +    {
        +      "type": "string"
        +    },
        +    {
        +      "type": "null"
        +    }
        +  ],
        +  "default": null,
        +  "title": "Message Text"
        +}
    • Changedread_board1 field changed
      • addedInput schema / properties / ats_job_id
        Added value: +{
        +  "anyOf": [
        +    {
        +      "type": "string"
        +    },
        +    {
        +      "type": "null"
        +    }
        +  ],
        +  "default": null,
        +  "title": "Ats Job Id"
        +}
    • Addedsalary_benchmark
    • Changedsearch_jobs3 fields changed
      • addedInput schema / properties / entry_level
        Added value: +{
        +  "default": false,
        +  "title": "Entry Level",
        +  "type": "boolean"
        +}
      • addedInput schema / properties / posted_within_days
        Added value: +{
        +  "anyOf": [
        +    {
        +      "type": "integer"
        +    },
        +    {
        +      "type": "null"
        +    }
        +  ],
        +  "default": null,
        +  "title": "Posted Within Days"
        +}
      • addedInput schema / properties / sector
        Added value: +{
        +  "anyOf": [
        +    {
        +      "type": "string"
        +    },
        +    {
        +      "type": "null"
        +    }
        +  ],
        +  "default": null,
        +  "title": "Sector"
        +}
  2. 1 tool updatev0.8.31
    • Changedsearch_jobs1 field changed
      • addedInput schema / properties / seniority
        Added value: +{
        +  "anyOf": [
        +    {
        +      "items": {
        +        "type": "string"
        +      },
        +      "type": "array"
        +    },
        +    {
        +      "type": "null"
        +    }
        +  ],
        +  "default": null,
        +  "title": "Seniority"
        +}
  3. 1 tool updatev0.8.27
    • Addedlist_portals
  4. 2 tool updatesv0.8.13
    • Addedadd_employer
    • Addedremove_employer
  5. 2 tool updatesv0.8.10
    • Addedcheck_employer
    • Addedread_board
  6. 1 tool updatev0.6.8
    • Changedsearch_jobs1 field changed
      • addedInput schema / properties / title
        Added value: +{
        +  "anyOf": [
        +    {
        +      "items": {
        +        "type": "string"
        +      },
        +      "type": "array"
        +    },
        +    {
        +      "type": "null"
        +    }
        +  ],
        +  "default": null,
        +  "title": "Title"
        +}
  7. 1 tool updatev0.6.7
    • Changedsearch_jobs1 field changed
      • addedInput schema / properties / location
        Added value: +{
        +  "anyOf": [
        +    {
        +      "type": "string"
        +    },
        +    {
        +      "type": "null"
        +    }
        +  ],
        +  "default": null,
        +  "title": "Location"
        +}
  8. 2 tool updatesv0.6.3
    • Addedrefresh_index
    • Changedsearch_jobs2 fields changed
      • addedInput schema / properties / collapse_role_group
        Added value: +{
        +  "default": false,
        +  "title": "Collapse Role Group",
        +  "type": "boolean"
        +}
      • addedInput schema / properties / company
        Added value: +{
        +  "anyOf": [
        +    {
        +      "type": "string"
        +    },
        +    {
        +      "type": "null"
        +    }
        +  ],
        +  "default": null,
        +  "title": "Company"
        +}
  9. 1 tool updatev0.5.0
    • Changedsearch_jobs1 field changed
      • addedInput schema / properties / offset
        Added value: +{
        +  "default": 0,
        +  "title": "Offset",
        +  "type": "integer"
        +}
  10. 5 tool updatesv0.3.2
    • First observedauthorize_application
    • First observedcheck_watches
    • First observedget_company_info
    • First observedsearch_jobs
    • First observedwatch_intent

TDQS

A4.4/5.0

Scored across 13 tools

Disambiguation4/5

Most tools have clearly distinct roles: search vs live board read (search_jobs / read_board), register vs pull watches (watch_intent / check_watches), add vs remove local employers, and application tracking vs authorization. The main soft spot is check_employer vs get_company_info, which both answer 'tell me about this company' from different angles; the descriptions distinguish them (due-diligence facts vs aggregate ghost/active-job stats) but an agent could plausibly pick the wrong one first.

Naming Consistency4/5

Nearly all names follow a snake_case verb_noun pattern (check_employer, search_jobs, read_board, add_employer, refresh_index, check_applications, authorize_application). Deviations are minor: watch_intent uses an unusual noun and salary_benchmark is a bare noun with no verb, but both remain readable and unambiguous.

Tool Count5/5

13 tools sit comfortably in the well-scoped range, and each maps to a distinct workflow step (search, vet, read a board, add/remove a source, watch, refresh, benchmark pay, track applications). No obvious filler or redundant tool.

Completeness4/5

Coverage is broad for the domain: discovery, employer vetting, external board reading, user-added sources, standing watches, refresh, salary benchmarking, and application lifecycle are all present. Minor gaps remain — there is no way to enumerate the indexed employers/portals as a list (only search by name or sector), and no single 'get this posting's full detail' tool outside read_board.

Maintenance

ActivityActive
ResponsivenessNo issues

Related MCP Connectors

Related MCP Servers

  • A
    license
    C
    quality
    C
    maintenance
    An MCP server that enables AI-assisted job search workflows including job discovery, application tracking, resume evaluation, and cover letter generation, with support for multiple job sources and scheduled scraping.
    83
    12 npm
    1
    AGPL 3.0
  • A
    license
    Not graded
    quality
    B
    maintenance
    A local-first, open-source MCP server that analyzes jobs, matches your CV, tailors documents, and tracks applications — all on your machine with no data uploaded.
    AGPL 3.0
  • A
    license
    Not graded
    quality
    C
    maintenance
    An MCP server that exposes job-search and application-management capabilities to compatible AI clients, enabling discovery of vacancies, drafting of tailored application materials, and coordinated human-approved submissions.
    MIT
  • A
    license
    Not graded
    quality
    D
    maintenance
    MCP server that enables AI assistants to search LinkedIn for job posts, save them locally, and manage them via a React dashboard.
    MIT