Build with Phantom
Connect your application or coding agent to hundreds of AI models through one API. Select a model directly, or use phantom-1 for automatic chat routing. Start with an API key and a working request, then explore routing, budgets, and signed receipts.
- Make your first API call
Configure a key and send a request.
- Connect a coding agent
Use the Phantom CLI with supported tools.
- Choose how models are selected
Compare phantom-1, auto, and explicit model IDs.
- Manage keys and spending
Set budgets and rotate credentials.
- Base URL
- https://phantom.codes/v1
- Auth
- Authorization: Bearer $PHANTOM_KEY
- API format
- OpenAI-compatible routes; /v1/messages is Anthropic-compatible. Keep your key secret.
Concepts: routing, receipts, and what links to a key.
Full reference: every field, header and error, with requests you can run.
Make your first request
Choose how you want to use Phantom. Both paths use the same account and balance.
API / SDK
- Get an API key: buy credit on /buy and copy the key it shows once, or sign in and create one on /keys. Keep it in an environment variable such as
PHANTOM_KEY. - Point an OpenAI SDK at
https://phantom.codes/v1with that key (examples in Call the API). - Send a chat request. Set
modelto a model ID, or tophantom-1to let Phantom choose. - Read what it cost and which model ran from the response headers (Response headers).
CLI / coding agent
- Install the Phantom CLI.
npm i -g @phantomcodes/clipnpm add -g @phantomcodes/clibun add -g @phantomcodes/cliphantom login: Sign in, or start a guest workspace.phantom fund --amount 10 --card: Add $10 in credit by card. Other supported payment methods are available.phantom claude: Start Claude Code using your Phantom balance. Codex and pi are supported too.
phantom login keeps the CLI's own key in the macOS Keychain, or in a private file on other systems; it doesn't print one for an SDK. Add credit when a call needs it: phantom-1:free runs without credit, within its rate caps.
Call the API from your code
- Get a key on /buy (card or crypto) or /keys.
- Point any OpenAI SDK at
https://phantom.codes/v1with your key. - Call
phantom-1. It works on every endpoint, and each endpoint runs its own model.
from openai import OpenAI
client = OpenAI(
base_url="https://phantom.codes/v1",
api_key="sk-phantom-your-api-key"
)
response = client.chat.completions.create(
model="phantom-1",
messages=[
{"role": "user", "content": "Hello!"}
],
stream=True
)
for chunk in response:
if chunk.choices:
print(chunk.choices[0].delta.content or "", end="")import OpenAI from 'openai';
const openai = new OpenAI({
baseURL: 'https://phantom.codes/v1',
apiKey: 'sk-phantom-your-api-key',
});
const stream = await openai.chat.completions.create({
model: 'phantom-1',
messages: [{ role: 'user', content: 'Hello!' }],
stream: true,
});
for await (const chunk of stream) {
process.stdout.write(chunk.choices?.[0]?.delta?.content ?? '');
}import { createOpenAI } from '@ai-sdk/openai';
import { streamText } from 'ai';
const phantom = createOpenAI({
baseURL: 'https://phantom.codes/v1',
apiKey: process.env.PHANTOM_KEY,
});
export async function POST(req: Request) {
const { prompt } = await req.json();
const result = streamText({
model: phantom('phantom-1'),
prompt,
});
return result.toUIMessageStreamResponse();
}import { ChatOpenAI } from '@langchain/openai';
import { HumanMessage } from '@langchain/core/messages';
const model = new ChatOpenAI({
model: 'phantom-1',
apiKey: process.env.PHANTOM_KEY,
configuration: {
baseURL: 'https://phantom.codes/v1',
},
});
const response = await model.invoke([
new HumanMessage('Hello!'),
]);
console.log(response.content);curl https://phantom.codes/v1/chat/completions \
-H "Content-Type: application/json" \
-H "Authorization: Bearer sk-phantom-your-api-key" \
-d '{
"model": "phantom-1",
"messages": [
{"role": "user", "content": "Hello!"}
],
"stream": true
}'Coding agents
# Installs the phantom-ai skill for Claude Code, Codex, Cursor and pi
npm i -g @phantomcodes/cli
phantom login # sign in by device code
phantom setup
phantom setup --agent claude --provider # Claude Code runs on Phantom AI# ~/.codex/config.toml
model = "phantom-1"
model_provider = "phantom"
[model_providers.phantom]
name = "Phantom AI"
base_url = "https://phantom.codes/codex/v1"
wire_api = "responses"
env_key = "PHANTOM_KEY"
model_catalog_url = "https://phantom.codes/codex/v1/models"
# Codex reads the model list only with this on
[features]
api_key_model_discovery = true
# then: export PHANTOM_KEY=sk-phantom-...// ~/.claude/settings.json
{
"env": {
"ANTHROPIC_BASE_URL": "https://phantom.codes/claude-code",
"ANTHROPIC_AUTH_TOKEN": "sk-phantom-...",
"ANTHROPIC_MODEL": "anthropic/claude-sonnet-5.5",
"ANTHROPIC_DEFAULT_HAIKU_MODEL": "phantom-1",
"CLAUDE_CODE_SUBAGENT_MODEL": "phantom-1",
"CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY": "1",
"CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS": "1"
}
}
// /model then lists Phantom models as claude-code/<id>// opencode.json
{
"$schema": "https://opencode.ai/config.json",
"provider": {
"phantom": {
"npm": "@ai-sdk/openai-compatible",
"name": "Phantom AI",
"options": {
"baseURL": "https://phantom.codes/coding-agent/v1",
"apiKey": "{env:PHANTOM_KEY}"
},
"models": {
"phantom-1": { "name": "Phantom 1" },
"anthropic/claude-sonnet-5.5": { "name": "Claude Sonnet 5.5" }
}
}
}
}API Provider: OpenAI Compatible
Base URL: https://phantom.codes/coding-agent/v1
API Key: sk-phantom-...
Model ID: phantom-1export OPENAI_API_BASE=https://phantom.codes/coding-agent/v1
export OPENAI_API_KEY=sk-phantom-...
aider --model openai/phantom-1Settings → Models
OpenAI API Key: sk-phantom-...
Override OpenAI Base URL: https://phantom.codes/cursor/v1
Add model: phantom-1
Chat only. Agent mode sends requests a custom base URL can't answer.Claude Code runs non-Claude models such as phantom-1, but Anthropic doesn't support that setup. The Phantom CLI on npm.
Choose how models are selected
- phantom-1
- Phantom's built-in selection. On chat, Messages and Responses it picks economy, standard or frontier for a new conversation. Chat with images, and each other endpoint, runs one model for that job.
- Configurable: Per request: zdr, no_training, x-phantom-free and x-phantom-escalate. A key's route policy can also set zdr and no_training. A key's lineup picks the model in each tier (PUT /v1/key/lineup).
- auto
- The key's route policy picks the model (GET, PUT, PATCH, DELETE /v1/key/route). On a key with no route policy, auto runs phantom-1's selection.
- Configurable: Yes: PUT /v1/key/route. Test it first with POST /v1/key/route/test, which charges nothing.
- An explicit model ID
- Calls that model directly, such as anthropic/claude-sonnet-5.5.
- Configurable: Skips phantom-1 selection. A route policy with applies_to: "all" still routes it.
A route policy normally routes only auto. With applies_to: "all" it also routes requests that name a model, except phantom-1, phantom-1:free and phantom-1:zdr on text chat, which always use their own selection.
Privacy controls
Phantom stores no prompt or completion text: it records the key, model, token counts and cost of each call, and routing keeps a salted hash of each conversation until its routing window ends. Model providers have their own retention and training policies; these controls choose which providers a request may use. Send zdr and no_training on a request, or set them in a key's route policy to apply them to every request on that key.
zdr: true(body): only providers that keep no data. It excludes the free model.no_training: true(body): only providers that don't train on prompts. It excludes the free model too.x-phantom-free: off(header): keep this request off the free model.x-phantom-content-logged(response header):false. It covers Phantom's own logging, not what a provider keeps.- To pick a model, phantom-1 and phantom-1:zdr (and auto on a key with no route policy) can send up to 1,000 characters of a conversation's first request to a model.
The full policy is on /privacy.
Model calls
/v1/chat/completionsphantom-1 chooses between economy, standard, and frontier models for the first request of a new chat conversation. Follow-up requests normally stay on the selected model. Request zero-data-retention routing with zdr: true: phantom-1 then skips the free model, and if the model it picks has no zero-data-retention provider the request fails with 400 zdr_unavailable. phantom-1:zdr picks only from models that have one. Use x-phantom-session when your client needs to keep the same task across requests. See Choose how models are selected and Privacy controls below.
curl https://phantom.codes/v1/chat/completions \
-H "Authorization: Bearer $PHANTOM_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "phantom-1",
"messages": [{"role": "user", "content": "Hello"}],
"stream": false
}'/v1/responsesOpenAI's Responses API, for Codex CLI and responses.create. store is always off; send the whole conversation in input.
curl https://phantom.codes/v1/responses \
-H "Authorization: Bearer $PHANTOM_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "phantom-1",
"input": "Write a haiku about receipts."
}'/v1/messagesThe Anthropic-compatible Messages endpoint. It takes your key in x-api-key or Authorization: Bearer. Claude Code: ANTHROPIC_BASE_URL=https://phantom.codes, ANTHROPIC_AUTH_TOKEN set to your key, and ENABLE_TOOL_SEARCH=true if you run MCP servers. Takes any model id, including auto; an Anthropic-style id such as claude-sonnet-5-5 maps to anthropic/claude-sonnet-5.5.
curl https://phantom.codes/v1/messages \
-H "x-api-key: $PHANTOM_KEY" \
-H "anthropic-version: 2023-06-01" \
-H "Content-Type: application/json" \
-d '{
"model": "claude-sonnet-5-5",
"max_tokens": 256,
"messages": [{"role": "user", "content": "Hello"}]
}'/v1/embeddingscurl https://phantom.codes/v1/embeddings \
-H "Authorization: Bearer $PHANTOM_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "phantom-1",
"input": "The quick brown fox"
}'/v1/rerankcurl https://phantom.codes/v1/rerank \
-H "Authorization: Bearer $PHANTOM_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "phantom-1",
"query": "capital of France",
"documents": ["Paris is the capital.", "Berlin is in Germany."],
"top_n": 2
}'/v1/images/generationscurl https://phantom.codes/v1/images/generations \
-H "Authorization: Bearer $PHANTOM_KEY" \
-H "Content-Type: application/json" \
-d '{
"prompt": "an isometric server room",
"n": 1,
"size": "1024x1024",
"response_format": "url"
}'/v1/video/generationsReturns a job id.
curl https://phantom.codes/v1/video/generations \
-H "Authorization: Bearer $PHANTOM_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "phantom-1",
"prompt": "a drone shot over a canyon",
"seconds": 5,
"aspect_ratio": "16:9"
}'/v1/video/generations/{id}Returns the receipt once the job completes.
curl https://phantom.codes/v1/video/generations/vid_123 \ -H "Authorization: Bearer $PHANTOM_KEY"
/v1/audio/speechcurl https://phantom.codes/v1/audio/speech \
-H "Authorization: Bearer $PHANTOM_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "phantom-1",
"input": "Hello from Phantom AI",
"voice": "alloy"
}' --output speech.mp3/v1/audio/transcriptionsSend as multipart/form-data.
curl https://phantom.codes/v1/audio/transcriptions \ -H "Authorization: Bearer $PHANTOM_KEY" \ -F file=@audio.mp3 \ -F model=openai/whisper-1
/v1/models/statusWhether a model is answering now: up, degraded or down, with each provider's uptime, speed and price. For phantom-1, job picks which of its models (chat, vision, embedding, rerank, image, video, speech, transcription). No key required.
curl "https://phantom.codes/v1/models/status?model=phantom-1&job=chat"
/v1/modelsNo key required.
curl https://phantom.codes/v1/models
/v1Lists the main routes. No key required.
curl https://phantom.codes/v1 -H "Accept: application/json"
Verification
/v1/receipts/keyPublic key for checking receipts. No key required.
curl https://phantom.codes/v1/receipts/key
/v1/receipts/anchorRecords the receipt hash on Solana. $0.01. Recording the same receipt again is free.
curl https://phantom.codes/v1/receipts/anchor \
-H "Authorization: Bearer $PHANTOM_KEY" \
-H "Content-Type: application/json" \
-d '{"receipt": "<x-phantom-receipt value>"}'/v1/receipts/anchorleaf is the receipt hash, the SHA-256 of the receipt string. No key required.
curl "https://phantom.codes/v1/receipts/anchor?leaf=<sha256-of-receipt>"
Keys
/v1/key/balancecurl https://phantom.codes/v1/key/balance \ -H "Authorization: Bearer $PHANTOM_KEY"
/v1/key/budgetcurl https://phantom.codes/v1/key/budget \ -H "Authorization: Bearer $PHANTOM_KEY"
/v1/key/budgetSet a cap per period (budget_usd) or a per-minute cap (rate_usd_per_min). period_days sets the period length; null means one month. Send null to remove a cap. Setting budget_usd starts a new period.
curl -X PATCH https://phantom.codes/v1/key/budget \
-H "Authorization: Bearer $PHANTOM_KEY" \
-H "Content-Type: application/json" \
-d '{"budget_usd": 20, "period_days": 30, "rate_usd_per_min": 0.10}'/v1/key/routeThe route policy that picks a model for model "auto".
curl https://phantom.codes/v1/key/route \ -H "Authorization: Bearer $PHANTOM_KEY"
/v1/key/routeReplace the route policy. models: 1 to 10 ids. Rules run in order; the first match picks the model, else models[0]. Conditions: pace, budget_left_pct_below, days_left_below, has_tools, input_tokens_over, reasoning_requested. use: a model from models, cheapest, first or next. Optional: applies_to (auto, all), on_empty (stop, cheapest), fallback_on_error.
curl -X PUT https://phantom.codes/v1/key/route \
-H "Authorization: Bearer $PHANTOM_KEY" \
-H "Content-Type: application/json" \
-d '{"models": ["anthropic/claude-sonnet-5.5", "phantom-1"],
"rules": [{"if": {"pace": "ahead"}, "use": "cheapest"}]}'/v1/key/routeUpdates the top-level fields you send and keeps the rest.
curl -X PATCH https://phantom.codes/v1/key/route \
-H "Authorization: Bearer $PHANTOM_KEY" \
-H "Content-Type: application/json" \
-d '{"on_empty": "cheapest"}'/v1/key/routeRemove the route policy.
curl -X DELETE https://phantom.codes/v1/key/route \ -H "Authorization: Bearer $PHANTOM_KEY"
/v1/key/route/testShows the model a chat request would run on now, and the rule that picked it. No charge.
curl -X POST https://phantom.codes/v1/key/route/test \
-H "Authorization: Bearer $PHANTOM_KEY" \
-H "Content-Type: application/json" \
-d '{"model": "auto", "messages": [{"role": "user", "content": "hi"}]}'/v1/key/lineupThe model each phantom-1 tier runs on this key, and the models each tier can run, with prices.
curl https://phantom.codes/v1/key/lineup \ -H "Authorization: Bearer $PHANTOM_KEY"
/v1/key/lineupPick the model for economy, standard or frontier from that tier's choices. A tier left out or null runs its default. fallbacks names the model to try when a tier's model fails.
curl -X PUT https://phantom.codes/v1/key/lineup \
-H "Authorization: Bearer $PHANTOM_KEY" \
-H "Content-Type: application/json" \
-d '{"frontier": "anthropic/claude-opus-5.5"}'/v1/key/lineupGo back to the default model in every tier.
curl -X DELETE https://phantom.codes/v1/key/lineup \ -H "Authorization: Bearer $PHANTOM_KEY"
/v1/keyCan't be undone. Deletes the key. Its leftover credit is lost. 409 while a request on it is running.
curl -X DELETE https://phantom.codes/v1/key \ -H "Authorization: Bearer $PHANTOM_KEY"
/v1/key/rotateIssues a new key with the same balance, expiry and caps. The old key stops working at once, so update every client that uses it right after.
curl -X POST https://phantom.codes/v1/key/rotate \ -H "Authorization: Bearer $PHANTOM_KEY"
/v1/privacyShows what is linked to your key or account. Send a key, a session cookie, or both.
curl https://phantom.codes/v1/privacy \ -H "Authorization: Bearer $PHANTOM_KEY"
Credit
/v1/bundlescurl https://phantom.codes/v1/bundles
/v1/coinscurl https://phantom.codes/v1/coins
/v1/x402/top-upPay with Solana USDC through pay (MPP or x402). The first call answers 402; pay pays it and sends the request again, which returns a key holding the credit, shown once with its recovery code. An empty body buys $1. Set target_api_key in the body to add the credit to a key you already have.
pay curl -X POST https://phantom.codes/v1/x402/top-up \
-d '{"amount_usd": 1}'/v1/purchase/solanaPay with USDC, USDT or SOL from a Solana wallet. coin: usdc, usdt or sol. Returns the recipient, the exact amount, a reference to attach to the transfer, and a Solana Pay link. Poll /v1/purchase/{id}/status, which checks the chain. Set target_api_key to add the credit to a key you already have.
curl https://phantom.codes/v1/purchase/solana \
-H "Content-Type: application/json" \
-d '{"amount_usd": 5, "coin": "usdc"}'/v1/purchase/cardPay by card. The card also pays Stripe's 2.9% + $0.30 fee, so $20 charges $20.91. From $50 Phantom pays the fixed part, so $50 charges $51.50. 1% bonus credit from $100, 2% from $500, on card, Solana and x402. Set target_api_key to add the credit to a key you already have. This links the payment to that key.
curl https://phantom.codes/v1/purchase/card \
-H "Content-Type: application/json" \
-d '{"amount_usd": 25, "validity_days": 365, "target_api_key": "sk-phantom-..."}'/v1/purchase/{id}/statusReturns the key once paid. Send the recovery code in the x-phantom-recovery-code header.
curl https://phantom.codes/v1/purchase/pay_123/status \ -H "x-phantom-recovery-code: 5xXj_4..."
/v1/purchase/recovercurl https://phantom.codes/v1/purchase/recover \
-H "Content-Type: application/json" \
-d '{"recovery_code": "5xXj_4..."}'Account
/v1/account/grantHistorical grant status. New grants are no longer available. Needs a session cookie.
curl https://phantom.codes/v1/account/grant \ -H "Cookie: better-auth.session_token=..."
/v1/accountDeletes grants and referrals and signs you out. API keys keep working: to stop one, call DELETE /v1/key with it, or rotate it.
curl -X DELETE https://phantom.codes/v1/account \ -H "Cookie: better-auth.session_token=..."
Response headers
x-phantom-request-id: req_4f1c9ab2d0e7
x-phantom-content-logged: false
x-phantom-receipt: <base64url payload>.<base64url signature>
# chat/completions
x-phantom-upstream-model: deepseek/deepseek-v4.1-flash
x-phantom-upstream-request-id: ...
# embeddings, rerank, images, audio, video
x-phantom-cost-usd: 0.000042
# every priced call, except a stream
x-phantom-balance-usd: 1.028916- Streaming receipts
- A stream sends the receipt as its last event,
event: phantom.receipt, right beforedata: [DONE]. - Receipt verification
- Check the signature in your own code with WebCrypto, or paste the receipt at /verify.
Generation parameters
| Parameter | Behavior |
|---|---|
reasoning_effort, thinking | Passed to the model provider unchanged. |
max_tokens, max_completion_tokens | Capped at what your balance can pay for and at the model's output limit. Included in the receipt's params_hash. |
model_served | The model name in the provider's response. /verify flags it when it differs from model_requested. |