The manager’s desk
Metrx treats the AI agents a company employs the way a line manager treats a team: a job contract, a scorecard against it, a review with labelled numbers, decisions on record. The desk is that review inside Claude Code (the plugin), inside any MCP client (the hosted server, workforce tool set) or over plain HTTP (three public routes). The sample review runs on the Northwind demo, which is synthetic demo data, and needs no account.
Install
claude plugin marketplace add metrxbots/mcp-server && claude plugin install metrx-desk@metrxbotsThe plugin adds six skills and connects to https://metrxbot.com/api/mcp?set=workforce. Without a key the public tools answer on the Northwind demo (synthetic demo data).
Optional environment variables:
METRX_API_KEY: a key from Settings → Security in the dashboard. Keys start withsk_live_.METRX_MCP_URL: override the server URL (self-hosted or staging).
Restart Claude Code after setting a variable.
Using Claude.ai, ChatGPT or Codex instead? Add the connector by URL and sign in, no key: see Connect from Claude.ai, ChatGPT or Codex.
Two other ways in:
Hosted MCP. Add the server to any MCP client that speaks streamable HTTP. The Authorization header is optional; without it the public tools answer on the demo.
{
"type": "http",
"url": "https://metrxbot.com/api/mcp?set=workforce",
"headers": { "Authorization": "Bearer sk_live_..." }
}stdio. npx @metrxbot/mcp-server --demo serves the same public tools over the demo with no key. npx @metrxbot/mcp-server with METRX_API_KEY set serves your own organization.
Skills
| Command | What it does | Needs a key |
|---|---|---|
/metrx-desk:sample-review | The Northwind sample review (synthetic demo data), relayed with every figure’s label | No |
/metrx-desk:draft-job-contract | Draft a job contract for one agent from a template (support_triage, sdr_qualification, internal_assistant, coding_patch); owner is required; publishing is a separate admin-scope step | No |
/metrx-desk:cost-per-resolution | The two numbers on a per-resolution AI bill: price per billed resolution and, with total conversations, the billed share. Both are the vendor’s own count, labelled vendor-reported, not Verified | No |
/metrx-desk:check-vendor-bill | Reconcile a per-resolution vendor bill against your own records: matched, disputed, unmatched, rejected, outside coverage, with the disputed amount | No |
/metrx-desk:connect-metrx | Create a key at Settings → Security, set METRX_API_KEY, restart, read your first review | Creates one |
/metrx-desk:weekly-decisions | Your decision queue; record expand, re_specify, combine or retire with a rationale | Yes (read; write to record) |
sample-review. You see one review as a manager would read it: each agent’s contract, what it cost, what it produced, and the figure for cost per confirmed outcome. Every number carries its label, and here every label is synthetic, because the Northwind organization is synthetic demo data. The skill relays the labels rather than hiding them. Use it to learn the shape of a review before you connect your own data.
draft-job-contract. You pick one of four templates and name an owner. The skill returns a draft contract: objective, unit of work, acceptance criteria, a KPI target, a budget and the human baseline if you gave one. Errors that would block publishing are listed next to warnings that would not. Nothing is published; that step needs a key with the admin scope and is yours to take.
cost-per-resolution. You give the bill total, the number of resolutions the vendor billed and the billing period; optionally the vendor’s own definition of resolved and the total conversations. The skill returns the effective price per billed resolution and, when you gave total conversations, the billed-resolution share. Your definition comes back word for word, marked as the vendor’s definition, not verified. Both numbers are the vendor’s own count, so they are labelled vendor-reported, never Verified. The answer always ends with the next step: send one export, the bill lines with ticket ids plus your ticket export for the same period, and Metrx recounts it with check-vendor-bill. The same page is a form at https://metrxbot.com/cost-per-resolution.
check-vendor-bill. You paste a vendor’s per-resolution bill and your own ledger of what was resolved, rejected or reopened. The skill sorts every billed line into matched, disputed (your records show it reopened), unmatched, rejected (a person overturned it) or outside coverage, and totals the disputed amount. While any line is rejected, or your importer reported rejected rows, the total difference is withheld (withheld: true, delta_usd: null) rather than stated on records that are known to be incomplete. The result is labelled measured because it is computed from the records you supplied. Lines outside the stated period are dropped and counted so you can see them.
connect-metrx. The skill walks you from no key to your first review. It tells you where the key is created (Settings → Security), which scope you need, where to set METRX_API_KEY, and reminds you to restart Claude Code. Then it reads your review and reports which of the three states short of ok applies, if any. It creates nothing on your behalf.
weekly-decisions. You see the queue of things waiting on a manager’s decision. This version knows seven queue kinds: no_contract, idle_agent, proposal, blocked_signal, cascade_approval, breaker_ack and expiring_verdict. Four more kinds (overlap, reviewer_low_catch, review_reduction, retry_loop) are defined but no code emits them yet. For each item you can record expand, re_specify, combine or retire with a rationale; recording needs a key with the write scope, reading needs read.
Labels
Every figure in a review carries one of seven labels.
- measured: computed from events Metrx received.
- owner-declared: a number the owner typed, such as a budget or a human baseline.
- advisory: a rule’s estimate, not yet measured.
- insufficient data: the figure cannot be formed yet; shown as words, never as a number.
- inferred: read off the data by a heuristic, for example a workload from review signals.
- proposal-not-measured: a proposed change whose savings have not run through a holdout yet; every number in a drafted contract carries it.
- synthetic: the Northwind demo, which is synthetic demo data.
Your own data
- Sign up at https://metrxbot.com/sign-up and create a key at Settings → Security. The
readscope covers the review and the queue,writerecords decisions,adminpublishes a contract. - Set
METRX_API_KEYin the environment Claude Code starts from, then restart it. - Send events with the Python SDK, the TypeScript SDK or OpenTelemetry. Snippets are at https://metrxbot.com/agent-quickstart.
- Confirm outcomes, with the SDK’s outcome call or a review signal, so cost per confirmed outcome can be measured. Until then the figure reads
insufficient data.
A review has three states short of ok:
no_events: nothing received yet. Check the SDK or OTel setup.no_outcomes: events flow, but no outcome has been confirmed. Cost is measured; cost per confirmed outcome is not.timeout: part of the read missed its deadline. The next read fills it in.
Grading desk
Signed in, Workforce in the dashboard shows your own review and one line above it: how many tasks from the last 7 days wait for a grade. The line opens Grading desk (/workforce/grade), where each task shows its agent, model, cost and join keys (task, session, request) next to the contract it is graded under: its quality measure and the outcomes it names as never acceptable. You grade a task approved, substantively_corrected or rejected, and can mark a breached guardrail as a critical defect. The grade is recorded as yours (reviewer_kind human), which makes the outcome human_verified, or error_flagged when the grade is rejected or a critical defect is marked.
- A task is offered only when its agent is live and has a published job contract. Agents without one are counted and linked to their contract page.
- One grade per task. Grading a task again replaces its grade (you confirm it first), and the change is audited.
- A grade judges the work, so it counts in the period the task ran (its last call), not the period the grade was given. A grade given or changed after a month closed restates that month when its numbers are read; no stored row is rewritten, and
regraded_atrecords when a grade last changed. A task with no recorded calls, or whose calls are stamped in the future, is dated when it is graded. Grades recorded before migration 156 keep the date they were last given, and from 156 on they too stand for their task in every period, so a task graded before 156 counts its outcomes in the period of that grade. - A
rejectedor critical-defect grade takes the task out of revenue: its other outcomes stop counting in the P&L statement and in the daily cost and revenue series, in every period. The agent scorecard counts a graded task by its grade alone, whatever the grade. - A desk grade governs its task. An API key cannot replace it or post any other verdict on that task, under any
reference_id; change it on the desk. - Only metadata is shown and stored: ids, counts, cost, timestamps. No prompt or response text.
Retiring an agent from its page (a retire decision) also archives it, so its tasks leave the queue. Retire needs an owner or admin, or an API key with the admin scope, and the product has no undo for it.
GET /api/v1/review/ungraded
curl -s 'https://metrxbot.com/api/v1/review/ungraded?days=7' -H 'Authorization: Bearer sk_live_...'The grading queue as data. Needs a session or a key with the read scope. Query: days (1 to 31, default 7, clamped to your plan’s history), agent_id, cursor, limit (1 to 100, default 25). Result in data: tasks[] {task_id, agent {id, agent_key, name}, model, event_count, cost_usd, first_seen, last_seen, session_id, gateway_request_id, contract_ids[]} newest first, contracts keyed by the ids in contract_ids {id, workload, version, scope, primary_quality, critical_guardrails}, next_cursor, capped (true when the queue’s read of your 20,000 most recent ungraded calls cut it short; older tasks appear as these are graded), empty_reason on an empty first page (all_graded, no_task_ids when your contracted agents’ calls carry no task_id, no_calls; otherwise null), window {since, until, days, clamped}, agents {eligible, ungoverned}. A read failure is a review-unavailable problem, never an empty queue.
POST /api/v1/signals/review
curl -s -X POST https://metrxbot.com/api/v1/signals/review \
-H 'Authorization: Bearer sk_live_...' -H 'Content-Type: application/json' \
-d '{"task_id":"<task uuid>","workload":"support_triage","agent_key":"support-triage","disposition":"approved","reviewer_kind":"human","reviewer_id":"qa@example.com"}'Records a grade. Needs a key with the write scope; a signed-in session grades as that person. Fields: task_id, workload (the contract it is graded under), agent_key, disposition, optional critical_defect {present, kind}, and, from a key, optional reviewer_kind (human makes the outcome human_verified; a judge model or no kind makes it review_linked) and reviewer_id. Every stored grade records where it came from (graded_via: desk or api_key). A session must send Content-Type: application/json, and a read-only member cannot grade. Answers: 200 recorded, 202 parked (the task’s events have not landed yet; it is recorded when they do), 403 read-only member, 404 the task is not in your org, 409 DESK_GRADE_LOCKED a person graded this task on the grading desk, and a key can neither replace that grade nor post another verdict on the task under any reference_id, 415 a session body not sent as JSON, 422 no governing contract for that workload, 503 the existing grade could not be checked (retry).
A grade cannot be edited with PATCH /api/outcomes/{id}: that route answers 409 REVIEW_OUTCOME_LOCKED for any outcome with source review. Change a grade on the desk or by posting the signal again.
HTTP routes
The same four public tools (metrx_get_workforce_review, metrx_draft_job_contract, metrx_cost_per_resolution, metrx_check_vendor_bill) as plain HTTP, described in OpenAPI at https://metrxbot.com/openapi-desk.json. Requests are rate limited per address at 30 per minute. The limit rides in the X-RateLimit-Limit and X-RateLimit-Remaining headers; X-RateLimit-Reset is present when the shared limiter answered (in the degraded in-process mode it is absent).
GET /api/v1/workforce-review
curl -s 'https://metrxbot.com/api/v1/workforce-review?org=demo'
curl -s 'https://metrxbot.com/api/v1/workforce-review?org=me' -H 'Authorization: Bearer sk_live_...'Query: org (demo by default, or me), cursor, limit (1 to 100, default 25).
POST /api/v1/contracts/draft
curl -s -X POST https://metrxbot.com/api/v1/contracts/draft \
-H 'Content-Type: application/json' \
-d '{"template":"support_triage","owner":"ops@example.com","agent_key":"support-triage","budget_usd":400,"budget_period":"monthly","human_baseline":{"cost_per_unit_usd":6.5,"minutes_per_unit":9,"basis":"2025 support team cost per resolved ticket"}}'Fields: template (required), owner (required), agent_key, workload, objective, unit_of_work, budget_usd, budget_period (daily, weekly, monthly), human_baseline {cost_per_unit_usd, minutes_per_unit, basis}, notes.
Result: draft, errors (these block publishing), warnings, label (proposal-not-measured), publish {method, path, scope "admin", policy_defaults "observe_only"}.
POST /api/v1/cost-per-resolution
curl -s -X POST https://metrxbot.com/api/v1/cost-per-resolution \
-H 'Content-Type: application/json' \
-d '{"bill_total":12400,"billed_resolutions":6200,"period":{"from":"2026-10-01","to":"2026-10-31"},"vendor":"Example vendor","vendor_resolved_definition":"A conversation the agent closed with no handoff.","total_conversations":10000}'Fields: bill_total (required, a number, not negative), billed_resolutions (required, a whole number, not negative), period {from, to} (required, dates such as 2026-10-01, from not after to), currency (three-letter code, USD by default), vendor, vendor_resolved_definition, total_conversations (a whole number, not negative).
Result: effective_price_per_resolution (null when billed_resolutions is 0), billed_resolution_share_pct (null without a usable total_conversations), vendor_definition {text, status} (the text exactly as sent), provenance {label "vendor-reported", verified false, statement}, warnings, and, always last, next_step {text, tool "metrx_check_vendor_bill", skill "check-vendor-bill", send}. A negative or non-numeric figure answers 400 with the field named.
POST /api/v1/vendor-bill/check
curl -s -X POST https://metrxbot.com/api/v1/vendor-bill/check \
-H 'Content-Type: application/json' \
-d '{"period":{"from":"2026-10-01T00:00:00Z","to":"2026-11-01T00:00:00Z"},"billed":[{"external_id":"T-1001","billed_at":"2026-10-03T10:00:00Z","cost_usd":1.5},{"external_id":"T-1002","billed_at":"2026-10-04T10:00:00Z","cost_usd":1.5}],"resolved_ids":["T-1001"],"ledger":[{"external_id":"T-1002","event_type":"reopened","occurred_at":"2026-10-05T08:00:00Z"}],"covered_spans":[{"from":"2026-10-01T00:00:00Z","to":null}]}'Fields: period {from, to} (from before to), billed[] {external_id, billed_at, cost_usd} with optional id, tier_name, adjustment_of (up to 500 lines), resolved_ids[], rejected_ids[], ledger[] {external_id, event_type, occurred_at} where event_type is solved, closed, reopened or public_comment (up to 2000 entries), import_rejected_count, covered_spans[] {from, to or null}.
Result: summary (counts per class: matched, disputed, unmatched, uncounted for rejected ids, outside_coverage; plus import_rejected, withheld, delta_count, their_usd, delta_usd, disputed_usd, unmatched_usd, outside_coverage_usd), rows (each with recon_class), dropped_outside_period, label (measured), note. delta_count and delta_usd are null while withheld is true: any uncounted line or a non-zero import_rejected_count withholds them.
Errors
Every refusal, over MCP or HTTP, carries a class. Over MCP it is a JSON-RPC error with data.class, data.doc, data.user_message and, depending on the class, retry_after_seconds, suggestions, fields or required_scope. Over HTTP it is an RFC 9457 problem body (type, title, status, detail, plus code and error) with a Retry-After header on the retryable classes.
auth-required
MCP code -32001, HTTP 401.
This tool needs a Metrx API key. Create one under Settings → Security and set METRX_API_KEY, or run the demo with org: “demo”.
Recovery: create a key, set the variable, restart. Or use the demo.
insufficient-scope
MCP code -32002, HTTP 403.
Your API key does not carry the scope this tool needs. Create a key with the named scope under Settings → Security.
Carries required_scope.
rate-limited
MCP code -32003, HTTP 429.
Too many requests from this address. Wait the number of seconds in retry_after_seconds and try again.
The public routes allow 30 requests per minute per address.
limiter-unavailable
MCP code -32004, HTTP 503.
The public desk is briefly unavailable while its rate limiter is down. Try again in about 30 seconds; a Metrx API key is not affected.
A Retry-After: 30 header and no retry_after_seconds field: the wait is a fixed outage interval, not a per-address window.
demo-unavailable
MCP code -32005, HTTP 503.
The Northwind demo is being rebuilt right now. Try again in a few minutes, or run the review on your own data with a Metrx API key.
The Northwind demo is synthetic demo data; a rebuild reseeds it. Retry-After: 300.
unknown-agent-key
MCP code -32006, HTTP 404.
No agent with that key in this organization. Check the agent_key on the Agents page; close matches, if any, are listed in suggestions.
Carries suggestions (up to 5).
invalid-params
MCP code -32602, HTTP 400.
One or more inputs are invalid; the fields list says which ones and why.
Carries fields as {name: [messages]}.
transport-error
MCP code -32007, HTTP 502.
Metrx could not be reached. Check METRX_MCP_URL (hosted) or your network, then try again.
review-unavailable
MCP code -32008, HTTP 503.
Your workforce review could not be built right now because one of its reads failed. Nothing was changed; try again in about a minute.
Only org=me answers this: one of the review’s reads (the spend aggregate or the agent roster) failed, or the sign-in lookup was briefly down. Carries retry_after_seconds and a Retry-After header (60, or 5 for a sign-in lookup).