Mint an API key and use it two ways: an OpenAI-compatible text API for your code and game servers, and an MCP server for agents — Claude Code, Cursor, n8n. Both run through the same pipeline, prices and wallet as the site. Every output is AI-generated content: label it as such wherever it gets published.
The four routing verbs are presets on these same three axes. Nothing here reads your prompt — the router weights the axes you choose, it does not try to guess your task.
Point any OpenAI SDK at this base URL and keep your code. model takes provider/model_name from the leaderboard, or let the votes decide: xd/auto, xd/fast, xd/budget or xd/max. The model that answered comes back in response.model, the real price in usage.cost_usd — on every response, streams included.
curl -s https://www.modelxd.com/api/v1/chat/completions \
-H "Authorization: Bearer <YOUR_KEY>" -H "Content-Type: application/json" \
-d '{"model":"xd/budget","messages":[{"role":"user","content":"One sentence: why blind votes?"}]}'from openai import OpenAI
client = OpenAI(base_url="https://www.modelxd.com/api/v1", api_key="<YOUR_KEY>")
r = client.chat.completions.create(model="xd/auto", messages=[{"role": "user", "content": "hi"}])
print(r.choices[0].message.content, r.usage)For agents in games: response_format with a json_schema is enforced server-side — a reply either matches your schema or you get a 422, never malformed text. models: [a, b] is an ordered fallback chain. Images and video are REST too: POST /api/v1/images/generations → poll /api/v1/jobs/{id}. Server-side keys only: there is no browser CORS, by design.
Create a key above and these fill in automatically; or replace <YOUR_KEY> by hand.
claude mcp add --transport http modelxd https://www.modelxd.com/api/mcp --header "Authorization: Bearer <YOUR_KEY>"
{
"mcpServers": {
"modelxd": {
"url": "https://www.modelxd.com/api/mcp",
"headers": {
"Authorization": "Bearer <YOUR_KEY>"
}
}
}
}curl -s https://www.modelxd.com/api/mcp -H "Authorization: Bearer <YOUR_KEY>" -H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'| get_leaderboard | models ranked by XD Score from real blind votes, with prices |
| pick_model | vote-backed recommendation for an image / video generation (for text, call the chat API with xd/auto) |
| generate_image | generate a still — returns outputs or a job_id |
| generate_video | generate a video — returns a job_id to poll |
| check_job | poll a generation until its outputs and actual cost land |
| get_balance | credit balance plus this key’s spend and cap |
Generations land in your XCreate gallery and Profile ledger like any other run. The spend cap is per key and lifetime, enforced up front: a call that would cross it is refused before it spends.
Everything below is the whole contract — every example has been run against production. Prices live on XBoard.
curl -s https://www.modelxd.com/api/v1/chat/completions \
-H "Authorization: Bearer xd_..." \
-H "Content-Type: application/json" \
-d '{
"model": "xd/auto",
"messages": [{"role": "user", "content": "One sentence: why blind votes?"}]
}'That's the whole integration: any OpenAI SDK, one base URL. There is no ModelXD SDK, deliberately — needing one would mean the compatibility failed.
Authorization: Bearer xd_...
Keys are minted on /xdev, shown once, stored hashed. Each key can carry a lifetime spend cap, enforced atomically before a call spends — ten concurrent requests cannot slip past it together.
Server-side only. The API sends no CORS headers, so a browser cannot call it — a key shipped in a client cannot be stolen from one. Keep the key on your server: game engine → your server → ModelXD.
/api/v1/chat/completionsSynchronous text inference, OpenAI-shaped in and out. Streaming via standard SSE.
| parameter | type | description | |
|---|---|---|---|
| model | string | required | A model slug or routing verb — see Models & routing. (Required unless models is given.) |
| models | string[] | optional | Ordered fallback chain, e.g. ["xai/grok-4.6", "xd/budget"]. First model that answers wins; 429s and provider failures move down the chain. xd.fallbacks in the response lists what was skipped and why. |
| messages | array | required | system / user / assistant. The system message rides each provider's native system slot — and its prompt cache — never the message array. |
| stream | boolean | optional | SSE chunks. The final chunk carries usage including cost_usd — no second request to learn the price. A request with a schema buffers instead (you cannot un-send a stream). |
| response_format | object | optional | {"type": "json_schema", ...}, enforced server-side — see Structured output. |
| max_tokens | integer | optional | Output cap; a sane per-model default otherwise. |
| xd | object | optional | {"effort": "low"|"medium"|"high"|"xhigh"|"max", "search": true}. Effort maps to the provider's thinking level; search enables web search on capable models (billed per search on top of tokens). Standard clients simply omit this. |
Not supported, loudly: tools / functions / tool_choice return an explicit 400 rather than prose that ignores your functions. For agent decisions use response_format — a filled-in form beats a function call.
| parameter | type | description | |
|---|---|---|---|
| provider/model_name | slug | optional | Exactly that model — google/gemini-3.6-flash, anthropic/claude-sonnet-5. Discover ids via GET /api/v1/models. |
| xd/auto | router | optional | Balanced — quality, price and measured first-token latency together. The everyday default. |
| xd/fast | router | optional | Lowest measured time to first visible token, above a quality bar. Ranked on each model’s SLOWEST thinking setting, so the speed holds however you call it. |
| xd/budget | router | optional | Cheapest by list price, above a quality bar. Routinely ~20× cheaper than xd/max; built for NPC crowds. |
| xd/max | router | optional | Highest blind-vote quality, price ignored entirely. |
The resolved model always comes back in response.model — you are never routed blind. An unknown, disabled, or API-blocked model is a 404 naming the model, never a silent substitution.
Agents with memory: resolve once, pin after. Call xd/auto when a character is created, read response.model, pin that slug for the session — switching models mid-conversation throws away the prompt cache on a history that only grows.
Ask for a JSON schema and the reply either validates against it or the call fails with 422 — never malformed text arriving at your validator as a surprise. One silent re-ask happens server-side first. The response's xd.structured_mode reports the enforcement tier: native_schema (constrained decoding), native_json (JSON guaranteed, schema checked by us), or coaxed (schema in the prompt, validated by us).
{
"model": "xd/budget",
"messages": [
{"role": "system", "content": "You are Rosa, a cautious farmer agent."},
{"role": "user", "content": "<world snapshot JSON>"}
],
"response_format": {"type": "json_schema", "json_schema": {
"name": "decision",
"schema": {
"type": "object",
"properties": {
"action": {"enum": ["plant","water","harvest","store","move_to",
"steal","guard","chase","flee","idle"]},
"target": {"type": "string"},
"amount": {"type": "integer", "maximum": 10},
"reason": {"type": "string"}
},
"required": ["action", "reason"],
"additionalProperties": false
}
}},
"xd": {"effort": "low"}
}Provider schema dialects differ (one rejects maximum, another requires every property in required) — ModelXD adapts the schema per provider and validates your original on the way back, so one schema means one thing even across a fallback chain. The decision arrives as a JSON string in choices[0].message.content: parse it, don't regex it.
/api/v1/images/generationsOpenAI-named so client.images.generate() finds it — but async: the answer is a 202 with a job id, not a finished file. Everything you can act on fails on this call — unknown model, empty prompt, exhausted balance, capped key — never as a job that dies later.
| parameter | type | description | |
|---|---|---|---|
| prompt | string | required | What to generate. |
| model | string | required | An image model slug, e.g. openai/gpt-image-2 — see ?type=image. |
| aspect_ratio | string | optional | e.g. 16:9, 1:1, 9:16. |
| size | string | optional | OpenAI's 1024x1024 form, accepted as an alias so OpenAI SDKs work unchanged. |
| quality | string | optional | low / medium / high. |
| n | integer | optional | Number of images, up to 4. |
POST https://www.modelxd.com/api/v1/images/generations
{ "model": "openai/gpt-image-2", "prompt": "a cheerful farm girl, low-poly",
"aspect_ratio": "16:9", "quality": "high" }
→ 202 { "id": "3f2b…", "object": "image.generation.job",
"status": "running", "poll": "/api/v1/jobs/3f2b…" }/api/v1/videos/generationsSame shape as images; video runs take minutes, so poll every ~15s.
| parameter | type | description | |
|---|---|---|---|
| prompt | string | required | What to generate. |
| model | string | required | A video model slug — see ?type=video. |
| duration | integer | optional | Seconds, 1–60, model-dependent range (commonly 4–15). |
| aspect_ratio | string | optional | e.g. 16:9, 9:16. |
| resolution | string | optional | e.g. 720p, 1080p where the model offers tiers. |
/api/v1/jobs/{id}→ { "id": "3f2b…", "object": "image.generation.job",
"status": "succeeded", // running | succeeded | failed
"model": "openai/gpt-image-2",
"data": [ { "url": "https://…signed…" } ],
"usage": { "cost_usd": 0.067 } }Fetch url promptly — generated files sit behind signed URLs that expire in ~24 hours. Everything also lands in your XCreate gallery, which never expires. On failed, the job carries an error message and costs nothing beyond what the provider actually burned.
/api/v1/jobs?type=image|video&limit=20Your recent generation jobs, newest first (limit ≤ 100). This is the recovery path: lose an id between the create and the first poll, and the job is still here — nothing has to be paid for twice. Files are not inlined; poll the one you want for a URL signed on the spot. Text runs are not listed — chat is synchronous and has no job.
/api/v1/models?type=text|image|videoOpenAI-shaped (client.models.list() works unchanged), and the only place a developer can discover ids like openai/gpt-image-2. Every callable model is listed — text, image and video — each row carrying modalities, an endpoint naming where to send it, ModelXD's display_name, pricing_usd_per_1m (null for per-output-priced image/video models — honest, not missing), and capabilities. The routers (xd/auto, xd/budget) appear under text.
{ "object": "list", "data": [
{ "id": "openai/gpt-5.6-sol", "object": "model", "owned_by": "openai",
"display_name": "GPT-5.6 Sol",
"pricing_usd_per_1m": { "input": 5, "output": 30 },
"capabilities": { "web_search": true, "structured_output": true, "vision": true } },
{ "id": "xd/auto", "object": "model", "owned_by": "modelxd", "tags": ["router"] }
] }OpenAI's envelope — {"error": {"message", "type", "code"}} — so SDK error handling works unmodified. Retry 429 / 5xx (429 carries Retry-After); never retry other 4xx unchanged.
| parameter | type | description | |
|---|---|---|---|
| 401 | auth | optional | Missing or revoked key. |
| 400 | request | optional | Malformed request — including tools (unsupported) and a bad response_format. |
| 402 | billing | optional | insufficient_credits (wallet empty) or spend_cap_reached (this key's cap). |
| 404 | model | optional | Unknown / disabled / blocked model, named in the message. |
| 422 | schema | optional | schema_unsatisfied — the model couldn't match your schema after the internal retry. Loosen the schema or try another model. |
| 429 / 5xx | transient | optional | Rate limited / provider failure — what models: [...] fallback absorbs for you. |
Calls debit your ModelXD wallet at the model's listed price — the same number XBoard shows, no API markup, ever. Every response reports its real cost in usage.cost_usd, streams included. Prompt caching on Anthropic-family models is applied automatically — keep your system message byte-stable and the saving shows up in the price, not in extra fields.
Ten agents thinking concurrently on one key is the designed load — nothing serializes, and the spend cap stays exact under parallel calls. There is no per-request rate limit today; the cap and your balance are the wall. New accounts start with $10 free credit; top-ups are 1:1 on Profile. Spend by day, model and key is on this page and at GET /api/v1/usage.
Every call made with a key is recorded: endpoint, model, tokens, list-price cost and whether it failed (failures cost $0). Read it from code with the same key:
/api/v1/usagecurl -s "https://www.modelxd.com/api/v1/usage?group_by=model&from=2026-09-01" \ -H "Authorization: Bearer $MODELXD_KEY"
| parameter | type | description | |
|---|---|---|---|
| from / to | date | optional | UTC date or ISO time. Default: the last 30 days; a bare `to` date includes that whole day. Up to 366 days. |
| group_by | string | optional | day (default, zero-filled), model, key, surface, or none for the request log. |
| key | string | optional | A key id, or self for the key making the request. Omit for all your keys. |
| surface | string | optional | chat, image, video or 3d. |
| limit / cursor | int / string | optional | Request-log paging (group_by=none): up to 500 per page; pass next_cursor back while has_more is true. |
The response carries totals for the whole window (requests, failed, input and output tokens, cost_usd) plus the grouped rows in data. Prices before you call are in GET /api/v1/models: per 1M tokens for text, pricing_usd_per_output per image or per video second.
Writing a program? Use the REST endpoints above. Connecting an agent that picks its own tools — Claude Code, Cursor, n8n? That's MCP. Same key, same billing:
claude mcp add --transport http modelxd https://www.modelxd.com/api/mcp \ --header "Authorization: Bearer xd_..."
| parameter | type | description | |
|---|---|---|---|
| get_leaderboard | tool | optional | Models ranked by XD Score, with prices and provider/model_name ids. |
| pick_model | tool | optional | Vote-backed recommendation for an image / video generation. |
| generate_image | tool | optional | Bills listed price. Fast models return outputs inline; slower ones a job_id. |
| generate_video | tool | optional | Always returns a job_id immediately. |
| check_job | tool | optional | Poll ~15s until outputs and the actual cost land. |
| get_balance | tool | optional | Wallet balance plus this key's spend and cap. |
All outputs are AI-generated content — label them as such wherever they get published.
Routing verbs and prices are live values, not promises — they move as votes land. Questions the docs don't answer: ask the agent on the home page.