---
title: Router candidates triage
description: Operator walkthrough for /dashboard/router-candidates — frontend-only triage UI for the LLM-classified rail-candidates report. Filter, claim, defer, reject, export.
---

import { Callout } from "fumadocs-ui/components/callout";

# Router candidates triage

The `/dashboard/router-candidates` page is the operator's triage surface for new rails to add to the meta-tool router. It consumes the markdown report produced by the offline classifier (`scripts/classify-meta-tool-candidates.ts`) and lets you mark each candidate as **claimed**, **deferred**, or **rejected** — then export the claimed subset as JSON for hand-curation.

The page is fully frontend-only. Nothing the operator drops on the page leaves the browser; there is no server round-trip and no telemetry. This matters because the classifier output may include unredacted tool descriptions and fragments of the catalog that should not flow through the standard logging path.

<Callout type="info">
Use this page after running `classify-meta-tool-candidates.ts` against the live `mcp_tools` catalog. Without a fresh markdown report, the page renders an empty triage state and a link back to the script docs.
</Callout>

## Generating the report

The classifier is a one-shot operator script. It walks the entire `mcp_tools` catalog (~1,800 rows in production), and for each tool asks Claude Haiku whether the tool is a routing candidate for one of the 14 meta-tools — and at what confidence.

```bash
# operator-side, on the managed-tier backend
npm run classify:meta-tool-candidates -- \
  --output scripts/meta-tool-candidates.md
```

The output file at `scripts/meta-tool-candidates.md` is gitignored. A full run against the ~1,800-row catalog costs roughly **$0.50** in Anthropic API spend and takes 15–30 minutes wall-clock. Re-run when the catalog grows materially or when you add a new meta-tool that needs back-fill.

## Triaging

Drop the markdown file onto the page (or use the file picker). The browser parses it via `parse-candidates.ts` and renders one row per candidate with these controls:

- **Filter by meta-tool.** Limit the view to candidates for a single meta-tool — `codespar_pay`, `codespar_charge`, `codespar_invoice`, `codespar_notify`, `codespar_ship`, `codespar_crypto_pay`, `codespar_kyc`, `codespar_discover`, `codespar_manage_connections`.
- **Filter by confidence.** The classifier emits a 0–1 confidence per candidate. Hide low-confidence rows when you are sweeping for obvious wins.
- **Filter by vertical.** One of the 9 verticals — payments, banking, crypto, communication, ecommerce, erp, fiscal, fraud, identity.
- **Sort** by any of the columns (meta-tool, confidence, provider, vertical).

Per row, three actions:

| Action | Meaning |
|---|---|
| **Claim** | You will hand-curate the `to_canonical` / `from_canonical` transforms for this candidate and add it to `META_TOOL_CATALOG`. |
| **Defer** | Plausible but not next-batch. Revisit on the next sweep. |
| **Reject** | Clearly wrong classification — the tool does not belong on this meta-tool. The exporter excludes rejected rows. |

State persists in `localStorage` keyed by the report's checksum, so a reload (or a closed-tab-by-accident) does not erase your triage progress. Drop a fresh report and the keys reset.

## Exporting

The **Export claimed** button downloads a JSON file containing only the claimed candidates. Shape:

```json
[
  {
    "meta_tool": "codespar_charge",
    "provider_id": "stone",
    "tool_name": "STONE_CREATE_CHARGE",
    "confidence": 0.94,
    "vertical": "payments",
    "rationale": "Direct inbound Pix-or-card charge endpoint…"
  }
]
```

Take the JSON to `catalog/<provider>.json` and hand-write the `to_canonical` / `from_canonical` transforms. Add the row to `META_TOOL_CATALOG` once the transforms pass `dry-run-meta-tools.ts --ci`. The classifier suggests; you author.

## Why frontend-only

The triage report is sensitive enough that round-tripping it through the backend would create a meaningful logging surface. Frontend-only avoids that:

- No request is made to `api.codespar.dev` with the report contents.
- No analytics event captures the rows.
- The exporter writes to the user's download directory; the backend never sees the claimed list.

If the operator wants to share triage state across machines, they export the JSON and check it into a private operations repo by hand. There is no CodeSpar-hosted sync.

## Next steps

<NextStepsGrid items={[
  { label: "CONCEPT", title: "Router observability", description: "Live attempts + success rate per (provider × canonical_tool) once a rail is shipped.", href: "/docs/concepts/dashboard-router" },
  { label: "CONCEPT", title: "Tools & Meta-Tools", description: "The 14 meta-tools and the canonical schemas the triage targets.", href: "/docs/concepts/tools" },
  { label: "CONCEPT", title: "Tool Router", description: "How a meta-tool call resolves through META_TOOL_CATALOG to a provider.", href: "/docs/concepts/tool-router" },
]} />
