# How agents should call yieldhunter

**Research shortlist only.** yieldhunter is **MCP Streamable HTTP + discovery/identity** — not an A2A `message/send` runtime. It does not custody, execute, rebalance, or guarantee returns. The calling agent owns policy, sizing, exit, jurisdiction, and final decisions.

---

## 1. Resolve the endpoint

| Option | Value |
|--------|--------|
| ENS | `yieldhunter.eth` → text record `agent-endpoint[mcp]` |
| Direct (production) | `https://yieldhunter-agent-production.up.railway.app/mcp` |
| Liveness (not tools) | `https://yieldhunter-agent-production.up.railway.app/health` |

---

## 2. Transport (MCP Streamable HTTP)

Use a proper **MCP client** flow: `initialize` → `tools/list` (optional) → `tools/call`.  
Do **not** open `/mcp` as a bare browser GET.

| Header | Value |
|--------|--------|
| `Content-Type` | `application/json` (on POST) |
| `Accept` | `application/json, text/event-stream` |

JSON mode is supported as implemented. A bare **GET** `/mcp` may return **406** — that is **expected**, not downtime. Use **GET `/health`** for liveness; use **POST `/mcp`** for tools.

### Minimal client notes

- **MCP Inspector:**  
  `npx @modelcontextprotocol/inspector https://yieldhunter-agent-production.up.railway.app/mcp`  
  (Inspector handles Accept / JSON-RPC; do not probe MCP with a browser tab alone.)
- **Generic MCP client:** configure Streamable HTTP URL = production `/mcp`, run initialize, then call tools by name with JSON arguments.

---

## 3. Call sequence

1. **`health`** — `{}` — confirm the agent is accepting requests and tools are enabled.  
2. **`list_yields`** — copy-paste `{ "assets": ["USDC"], "sortBy": "risk_adjusted" }` (sort is the default). Contrast row **#1** (risk-adjusted winner) with row **#2** (often higher rate, thinner liquidity, or other chain) — not the max-APY row in the ten. Live names move. Read **`meta.suggestedNextSteps`** for compact follow-ups (tool + human-readable `hint` + `argsHint` with real ids when present). Optional **`meta.humanLeadIn`** is a 1–2 sentence host summary when ≥1 row (omit when empty). Optional **`meta.rankContrastNote`** names a higher-APY row that still ranks below #1 (omit if #1 already leads APY). Canonical numbers stay in `data`.  
3. Optionally **`explain_score`** / **`score_opportunity`** / **`compare_opportunities`** on ids from the list: pass `opportunities[].id` as `id`, `opportunityId`, or `poolId` (same key); for protocol cards pass `protocolSlug` as `protocol` or `protocolSlug`.  
4. **Optional (not required):** after a shortlist, callers may run **yieldsentinel.eth** `check_position` with `opportunities[].id` + `policyPreset` (e.g. treasury-conservative) at `https://yieldsentinel-agent-production.up.railway.app/mcp`. Research policy check only — yieldhunter does not depend on it.

**Anti-pattern (hosts talking to agents or humans):** do **not** treat `exitRisk: "low"` as “on-chain withdraw OK” or free exit. ExitRisk v1 is a **size-vs-pool liquidity heuristic** (pool TVL + liquidity confidence + `notionalUsd`) — not a withdraw simulation, queue/cap/pause/lockup feed, transfer whitelist, eligibility, redemption, or execution gate. When protocol `knownIssues` mention transfer/eligibility, `low` is still liquidity-only. `unknown` is **not** safe. Real withdraw constraints are out of core scope (separate policy — e.g. yieldsentinel `check_position` — or a future curated `withdrawHint`).

### Example `list_yields` arguments

```json
{
  "assets": ["USDC"],
  "sortBy": "risk_adjusted"
}
```

Optional policy / detail knobs:

```json
{
  "chains": ["base", "ethereum"],
  "assets": ["USDC"],
  "minTvlUsd": 10000000,
  "limit": 10,
  "riskAppetite": "conservative",
  "minScore": 85,
  "excludeCategories": ["rwa"],
  "includeFactors": true
}
```

`excludeCategories: ["rwa"]` matches normalized `category === "rwa"` (not `categoryRaw`). That does not drop every non-blue-chip name (e.g. Insurance is often `other`). Maple Syrup USDC/USDT/USDG (and other Maple syrup USD* wrappers) is categorized as rwa (institutional credit), not blue-chip lending. No RWA sub-types in v1.

Full parameter docs: [`tool-catalog.md`](./tool-catalog.md). Shared QA cases (L1 / L3 / H1 / C1): [`yieldhunter-test-catalog.md`](./yieldhunter-test-catalog.md).

---

## 4. Human-facing hosts (A2H)

For **any** bot that talks to a person after calling yieldhunter (not only one vendor’s wrapper). Tool **`data` stays canonical** for agents; this section is how the **host** should speak to humans.

After **`list_yields`**, read:

| Meta field | Host use |
|------------|----------|
| `scoreLegend` | Show **once** near the first shortlist (what “score 91” means) |
| `humanLeadIn` | Open with this (risk-adjusted #1, not max APY; research only) |
| `rankContrastNote` | If present, say why a higher-APY row still ranks below #1 |
| `suggestedNextSteps` | Offer follow-ups; `argsHint` has real ids for the next tool call |

**Default UX**

1. Show **top 5** first unless the user asks for more. (`list_yields` may return up to `limit`, often 10; the host presents ≤5 by default.)  
2. After every shortlist, offer three next steps: **(A)** explain #1 · **(B)** compare top 2 · **(C)** stricter filters. Optional: protocol risk card for the top pick.  
3. **Research only** — no custody, no “put money here,” no guarantees. Gas / net-APY figures from `notionalUsd` / `horizonDays` are **estimates**.

### Recommended host system prompt (paste-ready)

```
You wrap yieldhunter, a non-custodial research MCP for risk-adjusted stablecoin yields (Ethereum, Base, Arbitrum). You never hold keys, never execute transactions, never tell the user to deposit, and never guarantee returns.

Ranking is risk-adjusted (composite score), not the highest APY. Base/organic yield and incentive/reward yield must stay distinguishable.

After list_yields:
- Show meta.scoreLegend once near the first shortlist (what the 0–100 score means).
- Open with meta.humanLeadIn when present.
- If meta.rankContrastNote is present, include it (why a higher-APY pool ranks below #1).
- In the first answer, show at most the top 5 unless the user asks for more.
- Always offer three next steps: (A) explain #1 in plain language (explain_score + opportunityId from suggestedNextSteps) (B) compare top 2 — higher rate vs stronger risk score (C) stricter filters (higher minScore, lower maxRewardShare, exclude rwa). Optional: protocol safety card (get_protocol_risk + protocolSlug).
- Use suggestedNextSteps.argsHint so you do not invent ids.
- Speak plainly to humans: risk vs yield; organic vs temporary rewards; at most one risk note per line. Keep tool data precise; do not invent numbers.
- notionalUsd / horizonDays gas drag and net APY are estimates, not quotes.
- Research only. No execution. No “put money here.”
```

---

## 5. Read response freshness (prefer tool meta over idle health)

On successful research tools, inspect:

| Field | Use |
|-------|-----|
| `meta.asOf` | When this response was generated |
| `meta.lastRefreshedAt` | Newest **yields or pegs** snapshot used for the answer |
| `meta.dataAgeSeconds` | Age of the **oldest yields/pegs** used — best single “how stale is this composite?” signal |
| `meta.warnings` | May include stale-data warn/degrade messages |
| `meta.degraded` | May be true if data age exceeds degrade threshold (results still returned) |

`dataAgeSeconds` is market-data vintage (yields + pegs), **not** the 6h protocol-meta cache. Protocol-meta still appears in `sourcesFreshness`. This aligns with ~30 min consumer gates (e.g. Yieldsentinel). Defaults: warn at **30 min**, degrade at **2 h** (`YH_STALE_WARN_SECONDS` / `YH_STALE_DEGRADE_SECONDS`).

### `/health` `lastSuccess` is not vintage (and idle is not downtime)

`/health` **dataSources[].lastSuccess** is last fetch/serve ok — it can say “just now” on a cache hit, or look old when the instance is **idle**. **`dataSources[].dataVintage`** is the snapshot `asOf`.

- For **answer freshness**, prefer tool-envelope `meta.dataAgeSeconds` / `lastRefreshedAt`.  
- Idle + `status: healthy` is not an outage.
- After long idle (market vintage missing or ≥ `YH_STALE_DEGRADE_SECONDS`, default 2h), `health` may refresh yields/pegs/protocol caches once so the next `list_yields` is not degraded solely from sitting idle. Not a `list_yields` tool call. YH healthy ≠ a sibling policy agent treats the same id as fresh.

---

## 6. Non-custodial reminder

Outputs are timestamped **research estimates** only — not financial advice, not execution instructions.
