← Xanadu · Tools · Dispatcher
Xanadu Dispatcher
Setup and operations for the open-source dispatcher from xanadu.run. Three sections: customer setup, operator playbook, reference. Read top to bottom the first time. Side nav for day-2 ops.
Sign up
Email hello@xanadu.run with your team name and we’ll mint a tenant_idon the spot (10 seconds, no credit card needed for the public beta). You’ll get back a ready-to-paste .mcp.json snippet.
Wire your orchestrator’s .mcp.json
{
"mcpServers": {
"xanadu": {
"type": "http",
"url": "https://xanadu.run/mcp",
"headers": {
"X-Xanadu-Tenant-Id": "your_tenant_id"
}
}
}
}Restart Claude Code / Cline / your orchestrator. You’ll see three MCP tools available:
mcp__xanadu__dispatch_agent
mcp__xanadu__list_agents
mcp__xanadu__ledger_summaryFrom inside your session, ask in plain language:
"use the code-reviewer agent via xanadu to review the recent diff"BYOT. Bring your own provider keys.
Add X-Anthropic-Key and/or X-OpenRouter-Key to your .mcp.json headers. Every dispatch then bills against your provider quota; we only collect the per-token routing fee. Keys are held in a per-request context for the lifetime of the HTTP request and never persisted.
"headers": {
"X-Xanadu-Tenant-Id": "your_tenant_id",
"X-Anthropic-Key": "sk-ant-...",
"X-OpenRouter-Key": "sk-or-v1-..."
}secrets: allowlist. A key for a provider the dispatched agent isn’t configured for is silently dropped (and WARN-logged so operators see the mismatch). That stops a hostile client from tricking a Claude-only agent into making OpenRouter calls.Available agents
| name | role | model |
|---|---|---|
ticket-implementer | spec → multi-file change → PR, 60-min cap, $5 budget | anthropic/claude-opus-4.7 |
code-reviewer | focused review, verdict line | anthropic/claude-sonnet-4.6 |
refactorer | behavior-preserving refactor | anthropic/claude-sonnet-4.6 |
test-writer | tests matching project conventions | anthropic/claude-sonnet-4.6 |
git-committer | drafts a commit message from a staged diff | anthropic/claude-haiku-4.5 |
kimi-reviewer | same review duty via OpenRouter to Kimi K2.6 (self-host only, followup #61) | openrouter/moonshotai/kimi-k2.6 |
xanadu.run/mcp, any agent configured with backend: opencodeis transparently routed through ClaudeBackend until upstream fixes the opencode-on-Fly silent-zero bug (followup #61). The substitution is recorded on the ledger row’s extra.substituted_from and on the returned DispatchResult.substituted_from. For openrouter/anthropic/* models the matching Anthropic-direct id is used; anything else falls back to anthropic/claude-sonnet-4.6. Self-host is unaffected.Operator: prerequisites
You’re running Xanadu in hosted mode on Fly (or any container host) and taking customers. Required env / secrets:
OPENROUTER_API_KEY: foropenrouter/*models via opencodeANTHROPIC_API_KEY: foranthropic/*models via the Claude CLI (the currently-working billing path on Fly firecracker VMs)XANADU_REQUIRE_TENANT=1: hard-fail any dispatch missing atenant_idXANADU_ADMIN_TOKEN: bearer secret forPOST /admin/tenants(unset returns 503)CLAUDE_CODE_SIMPLE=1: forces Claude CLI into API-key auth instead of interactive OAuth (already baked into the Docker image)
After secrets land, flyctl ssh console -C "xanadu doctor" should report OK on every check.
Onboard a customer
Two commands from inside the running container:
flyctl ssh console -a xanadu-mcp
xanadu tenants add cust_42 \
--name "Customer Name" \
--contact "alice@customer.example" \
--markup 0.10 \
--monthly-cap 500.00 \
--tenants /data/tenants.json
xanadu tenants config cust_42 \
--tenants /data/tenants.json \
--format textThe second command prints the customer-facing setup blurb plus the exact .mcp.json blob. Copy, email, done. TenantRegistry.reload_if_changed() stat()s the file on every dispatch, so the next inbound call serves the new tenant. No restart needed.
Or via HTTP (signup form / Stripe webhook / CF Worker)
curl -X POST https://xanadu.run/admin/tenants \
-H "Authorization: Bearer $XANADU_ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"Customer Name","markup_rate":0.10,"monthly_budget_usd":500.0}'
# Response includes the pasteable .mcp.json blob
# {"tenant_id":"cust_abc123","mcp_json":{...}}Per-tenant visibility
xanadu tenants info cust_42 --tenants /data/tenants.json
# title verdict: "clean" / "93% used" / "OVER CAP"
# body: config + MTD + remaining + failures-this-month + last 5 dispatches
xanadu ledger by-tenant --current-month --tenants /data/tenants.json
# Sorted by billed spend so loudest customers float to the topCaps that enforce
Per-agent and per-tenant monthly_budget_usd are enforced at dispatch time. Two checks fire:
- Pre-sizing short-circuit: if MTD already meets/exceeds the cap, refuse before paying for sizing or spawning the backend.
- Post-estimate gate: if MTD +
est_max_usdwould exceed, refuse. Held underledger.exclusive_lock()so concurrent dispatches can’t both pass when only one fits.
Refusals write a structured budget_exceeded ledger row with the cap, MTD, and tenant_id; the customer sees a clear BudgetExceededError at the MCP layer.
Invoicing flow
# 1. Pre-flight: catches markup drift, MTD over cap, orphan tenants.
xanadu billing audit --current-month --tenants /data/tenants.json
# 2. Dry-run the Stripe push. See what would POST.
xanadu billing push --current-month --map /etc/xanadu/si_map.json --tenants /data/tenants.json
# 3. Commit when audit is clean.
xanadu billing push --current-month --commit --map /etc/xanadu/si_map.jsonsi_map.json is {tenant_id: subscription_item_id}. push refuses to send if any tenant in the window lacks a Stripe SI mapping. Silent skips were the worst billing failure mode.
Deprovision a tenant
xanadu tenants remove cust_42 --yes --tenants /data/tenants.jsonHot-reload removes them from the live registry immediately. Existing ledger rows are kept (append-only) and show up under (unknown) in subsequent by-tenant rollups so the final invoice can still be audited.
Doctor. Start every triage here.
flyctl ssh console -a xanadu-mcp -C "xanadu doctor"Walks the deploy and flags every misconfiguration that has caused production pain. Hosted-mode-relevant checks:
tenants_file: exists + parseshosted_mode_coherence: strict-tenant mode + at least one usable provider keyclaude_code_simple: Claude CLI auth modeapi_keys: at least one provider key presentbackend_cli:claudeand/oropencodeon PATH
MCP tools
| tool | does |
|---|---|
dispatch_agent | Run an agent with a task; returns text + cost + dispatch_id. |
list_agents | Catalog of available agents (name, description, default_model, budget). |
ledger_summary | Aggregate ledger across all dispatches; totals by agent / model / status / tenant. |
HTTP headers
| header | role |
|---|---|
X-Xanadu-Tenant-Id | Required in XANADU_REQUIRE_TENANT mode. Bound to a request-scoped ContextVar; dispatch_agent picks it up automatically. |
X-Anthropic-Key | Optional BYOT. Customer’s Anthropic key forwarded into the backend subprocess for this request only. |
X-OpenRouter-Key | Optional BYOT. Same, for OpenRouter. |
Authorization: Bearer … | Only used by POST /admin/tenants; auth is timing-safe via secrets.compare_digest. |
Built-in model pricing
Used to budget-gate before dispatch. When the backend reports a provider-billed cost, we bill on the provider’s number instead and keep this only for estimation.
| model | $/M in | $/M out |
|---|---|---|
anthropic/claude-opus-4.7 | 5.00 | 25.00 |
anthropic/claude-sonnet-4.6 | 3.00 | 15.00 |
anthropic/claude-haiku-4.5 | 1.00 | 5.00 |
openrouter/moonshotai/kimi-k2.6 | 0.684 | 3.42 |
openrouter/deepseek/deepseek-v4-pro | 0.435 | 0.87 |
openrouter/qwen/qwen3-coder-next | 0.11 | 0.80 |