Xanadu
On this page

← 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_summary

From 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-..."
}
The header override is gated by the agent’s 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

namerolemodel
ticket-implementerspec → multi-file change → PR, 60-min cap, $5 budgetanthropic/claude-opus-4.7
code-reviewerfocused review, verdict lineanthropic/claude-sonnet-4.6
refactorerbehavior-preserving refactoranthropic/claude-sonnet-4.6
test-writertests matching project conventionsanthropic/claude-sonnet-4.6
git-committerdrafts a commit message from a staged diffanthropic/claude-haiku-4.5
kimi-reviewersame review duty via OpenRouter to Kimi K2.6 (self-host only, followup #61)openrouter/moonshotai/kimi-k2.6
Hosted-mode note: On 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: for openrouter/* models via opencode
  • ANTHROPIC_API_KEY: for anthropic/* models via the Claude CLI (the currently-working billing path on Fly firecracker VMs)
  • XANADU_REQUIRE_TENANT=1: hard-fail any dispatch missing a tenant_id
  • XANADU_ADMIN_TOKEN: bearer secret for POST /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 text

The 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 top

Caps that enforce

Per-agent and per-tenant monthly_budget_usd are enforced at dispatch time. Two checks fire:

  1. Pre-sizing short-circuit: if MTD already meets/exceeds the cap, refuse before paying for sizing or spawning the backend.
  2. Post-estimate gate: if MTD + est_max_usd would exceed, refuse. Held under ledger.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.json

si_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.json

Hot-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 + parses
  • hosted_mode_coherence: strict-tenant mode + at least one usable provider key
  • claude_code_simple: Claude CLI auth mode
  • api_keys: at least one provider key present
  • backend_cli: claude and/or opencode on PATH

MCP tools

tooldoes
dispatch_agentRun an agent with a task; returns text + cost + dispatch_id.
list_agentsCatalog of available agents (name, description, default_model, budget).
ledger_summaryAggregate ledger across all dispatches; totals by agent / model / status / tenant.

HTTP headers

headerrole
X-Xanadu-Tenant-IdRequired in XANADU_REQUIRE_TENANT mode. Bound to a request-scoped ContextVar; dispatch_agent picks it up automatically.
X-Anthropic-KeyOptional BYOT. Customer’s Anthropic key forwarded into the backend subprocess for this request only.
X-OpenRouter-KeyOptional 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.75.0025.00
anthropic/claude-sonnet-4.63.0015.00
anthropic/claude-haiku-4.51.005.00
openrouter/moonshotai/kimi-k2.60.6843.42
openrouter/deepseek/deepseek-v4-pro0.4350.87
openrouter/qwen/qwen3-coder-next0.110.80