// DOCUMENTATION
PHANTOM API
OpenAI-compatible. Anonymous payment. Hardware-attested inference.
// CONNECTION
| Base URL | https://phantom.codes/v1 |
| Tor (.onion) | http://jzqvbfmrlt5ye467joz75dg6xdurc6bniozautqlil5b3tbf777zmsid.onion/v1 |
| Auth header | Authorization: Bearer sk-... |
| Get a key | Buy credit at phantom.codes. Pay XMR / BTC / ETH / USDT / USDC / LTC / SOL / DOGE. Receive sk-... once. |
| Compatibility | Drop-in OpenAI replacement. Point any OpenAI SDK at the Base URL above. |
// 01
Quick start
▸ curl
$ curl https://phantom.codes/v1/chat/completions \
-H "Authorization: Bearer YOUR_PHANTOM_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "phantom/qwen3-32b",
"messages": [{"role":"user","content":"Hello"}]
}'
▸ Python
from openai import OpenAI
client = OpenAI(
api_key="YOUR_PHANTOM_KEY",
base_url="https://phantom.codes/v1",
)
r = client.chat.completions.create(
model="phantom/deepseek-v4-flash",
messages=[{"role": "user", "content": "Hello"}],
)
print(r.choices[0].message.content)
▸ Node
import OpenAI from "openai";
const client = new OpenAI({
apiKey: "YOUR_PHANTOM_KEY",
baseURL: "https://phantom.codes/v1",
});
const r = await client.chat.completions.create({
model: "phantom/qwen3.5-122b-a10b",
messages: [{ role: "user", content: "Hello" }],
});
console.log(r.choices[0].message.content);
Need crypto? See /#pricing for accepted coins and how to acquire them.
// 02
Endpoints
| METHOD | PATH | AUTH | PURPOSE |
|---|---|---|---|
POST | /v1/chat/completions | Bearer or x402 | OpenAI-compatible chat. Streams. Vision via phantom/qwen3.6-27b using image_url parts. |
POST | /v1/embeddings | Bearer | OpenAI-compatible embeddings. |
POST | /v1/images/generations | Bearer | Image generation. Flat per-image billing. |
GET | /v1/models | none | List models + live pricing (markup included). |
POST | /v1/purchase | none | Body: {"bundle":"small"} OR {"amount_usd":7.5}. |
GET | /v1/purchase/{id}/status | none | Poll payment. Returns plaintext key once on completion. |
GET | /v1/key/balance | Bearer | Remaining credit + expiry. |
POST | /v1/key/rotate | Bearer | Issue new key, carry credit, deactivate old. |
GET | /v1/signature/{id} | Bearer | Per-response signature. Step 2 of attestation. |
GET | /v1/inference-attest | Bearer | TDX + NVIDIA CC report. Step 3 of attestation. |
GET | /health | none | Liveness. |
▸ Check balance
$ curl https://phantom.codes/v1/key/balance \
-H "Authorization: Bearer YOUR_PHANTOM_KEY"
{"balance_usd": 9.7421, "expires_at": "2026-08-25T18:00:00+00:00"}
▸ Rotate key
$ curl -X POST https://phantom.codes/v1/key/rotate \
-H "Authorization: Bearer YOUR_PHANTOM_KEY"
{"api_key": "sk-...", "balance_usd": 9.7421}
Old key dies instantly. Plaintext returned exactly once. Store it.
// 03
Streaming
Pass "stream": true. SSE-compatible. Each chunk arrives as data: {...} followed by a blank line. Final chunk before data: [DONE] carries the usage block used for billing.
$ curl -N https://phantom.codes/v1/chat/completions \
-H "Authorization: Bearer YOUR_PHANTOM_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "phantom/qwen3.5-122b-a10b",
"stream": true,
"messages": [{"role":"user","content":"Hi"}]
}'
data: {"id":"chatcmpl-...","choices":[{"delta":{"content":"He"}}]}
data: {"id":"chatcmpl-...","choices":[{"delta":{"content":"llo"}}]}
data: {"id":"chatcmpl-...","choices":[{"delta":{},"finish_reason":"stop"}],"usage":{"prompt_tokens":8,"completion_tokens":2,"total_tokens":10}}
data: [DONE]
Abort mid-stream: phantom bills for tokens already generated. If upstream sent no bytes, no charge. Intentional aborts before max_tokens finishes are still billed.
// 04
Image generation
OpenAI-compatible. Flat-rate per image. Upstream URLs live ~1 hour. Download them.
$ curl https://phantom.codes/v1/images/generations \
-H "Authorization: Bearer YOUR_PHANTOM_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "stability/stable-diffusion-3-5-large",
"prompt": "A misty cyberpunk alley at dawn",
"n": 1,
"size": "1024x1024",
"quality": "standard"
}'
Parameters: model, prompt (max 4000 chars), n (1-10), size (256/512/1024/1536/2048 square), quality (standard | hd), response_format (url | b64_json). Per-model pricing in /v1/models under image_pricing_usd_user.
// PRIVACY NOTE
All image models are tier PROXY. Gateway runs in TDX but the model itself runs on the vendor's infra. They see your prompt content. Phantom hides only your identity.
// 05
Attestation
Bind each response to verified TEE hardware in three steps.
1. Run any chat completion. Capture response.id.
2. GET /v1/signature/{id}?model=... → returns signature + signing_address.
3. GET /v1/inference-attest?model=...&nonce=$(openssl rand -hex 32)&signing_address=... → returns Intel TDX quote + NVIDIA CC payload.
Verify offline:
- TDX quote →
POST https://phantom.codes/v1/verify/tdx - NVIDIA payload →
POST https://phantom.codes/v1/verify/gpu - Signature → recover pubkey, confirm matches
signing_address
Chain: response → signature → TEE quote.
// 06
Integrations
Dedicated coding-agents page with full setups: /code.html.
Anything that takes base_url + api_key works. Set:
$ export OPENAI_BASE_URL=https://phantom.codes/v1
$ export OPENAI_API_KEY=sk-your-phantom-key
Picks up automatically in: LangChain, LlamaIndex, Vercel AI SDK, Pydantic-AI, llm, mods, aichat, shell-gpt, tgpt, chatblade.
▸ Tool-by-tool config
Aider:
aider --openai-api-base https://phantom.codes/v1 \
--openai-api-key sk-your-phantom-key \
--model openai/phantom/deepseek-v4-flash
OpenHands:
export LLM_API_KEY=sk-your-phantom-key
export LLM_BASE_URL=https://phantom.codes/v1
export LLM_MODEL=openai/phantom/deepseek-v4-flash
opencode · ~/.config/opencode/opencode.json:
{
"provider": {
"phantom": {
"npm": "@ai-sdk/openai-compatible",
"options": {
"baseURL": "https://phantom.codes/v1",
"apiKey": "{env:OPENAI_API_KEY}"
},
"models": {
"phantom/deepseek-v4-flash": {"name": "DeepSeek V4 Flash"}
}
}
}
}
Crush · ./crush.json:
{
"$schema": "https://charm.land/crush.json",
"providers": {
"phantom": {
"type": "openai-compat",
"base_url": "https://phantom.codes/v1",
"api_key": "$OPENAI_API_KEY",
"models": [
{
"id": "phantom/deepseek-v4-flash",
"name": "DeepSeek V4 Flash",
"context_window": 1048576,
"default_max_tokens": 8192
}
]
}
}
}
type must be "openai-compat" — "openai" is reserved for OpenAI itself. Leave models empty and Crush pulls the whole catalog from /v1/models.
Continue.dev · ~/.continue/config.json:
{
"models": [{
"title": "Phantom DeepSeek",
"provider": "openai",
"model": "phantom/deepseek-v4-flash",
"apiKey": "sk-your-phantom-key",
"apiBase": "https://phantom.codes/v1"
}]
}
Cline / Cursor / OpenWebUI: pick "OpenAI Compatible" in settings. Base URL = https://phantom.codes/v1, API Key = your sk-..., Model = any from /v1/models.
// FUNCTION-CALLING
For agents that lean on tools, pick a capable model: phantom/deepseek-v4-pro, phantom/glm-5.2 (strong), phantom/deepseek-v4-flash, phantom/qwen3.5-122b-a10b (decent). OpenAI-shape tools[] works as-is:
{
"model": "phantom/deepseek-v4-flash",
"messages": [{"role": "user", "content": "Weather in Tokyo?"}],
"tools": [{
"type": "function",
"function": {
"name": "get_weather",
"description": "Current weather for a city",
"parameters": {
"type": "object",
"properties": {"city": {"type": "string"}},
"required": ["city"]
}
}
}],
"tool_choice": "auto"
}
Model returns tool_calls in choices[0].message. Send back a {"role":"tool","tool_call_id":"...","content":"..."} message in the next turn.
// 07
Pay per request (x402)
No account, no key, no pass. Send the completion request with no Authorization header, get a price back, attach a signed USDC payment, get the answer. Built for agents, which cannot buy a bundle or hold a pass code.
USDC on Base. The payment is an EIP-3009 authorization: a signature, not a transfer, so nothing waits for a block confirmation. Phantom verifies it in-process, so no facilitator sees your address or what you bought.
▸ 1. Ask, unauthenticated
$ curl -i https://phantom.codes/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "phantom/gpt-oss-120b",
"max_tokens": 64,
"messages": [{"role":"user","content":"Hello"}]
}'
HTTP/2 402
payment-required: eyJ4NDAyVmVyc2lvbiI6Miwi...
{"x402Version": 1,
"error": "X-PAYMENT header is required",
"accepts": [{
"scheme": "exact",
"network": "base",
"maxAmountRequired": "10000",
"payTo": "0x...",
"asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"maxTimeoutSeconds": 60,
"extra": {"name": "USD Coin", "version": "2"}
}]}
maxAmountRequired is in atomic units. USDC has 6 decimals, so 10000 = $0.01. Both protocol versions are served: v1 reads the body and sends X-PAYMENT, v2 reads the PAYMENT-REQUIRED header and sends PAYMENT-SIGNATURE.
▸ 2. Sign it and ask again
Any x402 client works. Hand-rolled, in forty lines of httpx + eth-account:
import base64, json, secrets, time, httpx
from eth_account import Account
from eth_account.messages import encode_typed_data
TYPES = {"TransferWithAuthorization": [
{"name": "from", "type": "address"},
{"name": "to", "type": "address"},
{"name": "value", "type": "uint256"},
{"name": "validAfter", "type": "uint256"},
{"name": "validBefore", "type": "uint256"},
{"name": "nonce", "type": "bytes32"},
]}
def sign_payment(account, offer):
amount = int(offer["maxAmountRequired"])
now, nonce = int(time.time()), secrets.token_bytes(32)
auth = {"from": account.address, "to": offer["payTo"],
"value": amount, "validAfter": now - 600,
"validBefore": now + offer["maxTimeoutSeconds"],
"nonce": nonce}
signed = account.sign_message(encode_typed_data(
domain_data={"name": offer["extra"]["name"],
"version": offer["extra"]["version"],
"chainId": 8453,
"verifyingContract": offer["asset"]},
message_types=TYPES, message_data=auth))
payload = {"x402Version": 1, "scheme": "exact",
"network": offer["network"],
"payload": {"signature": "0x" + signed.signature.hex().removeprefix("0x"),
"authorization": {**auth, "value": str(amount),
"validAfter": str(auth["validAfter"]),
"validBefore": str(auth["validBefore"]),
"nonce": "0x" + nonce.hex()}}}
return base64.b64encode(json.dumps(payload).encode()).decode()
class X402Auth(httpx.Auth):
"""Pay on 402, retry once. Every retry signs a FRESH authorization."""
requires_response_body = True
def __init__(self, account): self.account = account
def auth_flow(self, request):
response = yield request
if response.status_code != 402: return
offer = json.loads(response.read())["accepts"][0]
request.headers["X-PAYMENT"] = sign_payment(self.account, offer)
yield request
▸ 3. Point an agent at it
Because it is an httpx.Auth, it drops into the OpenAI SDK, and therefore into LangChain, LlamaIndex, Pydantic-AI, or anything else built on that SDK. No API key anywhere:
from openai import OpenAI
client = OpenAI(
api_key="unused", # the wallet is the credential
base_url="https://phantom.codes/v1",
http_client=httpx.Client(auth=X402Auth(Account.from_key(PRIVATE_KEY))),
)
r = client.chat.completions.create(
model="phantom/gpt-oss-120b",
max_tokens=64,
messages=[{"role": "user", "content": "Hello"}],
)
LangChain: ChatOpenAI(base_url=..., api_key="unused", http_client=httpx.Client(auth=X402Auth(acct))). Runnable version of all of the above: scripts/x402-client-demo.py in the repo.
// RULES OF THE RAIL
- One payment, one request. Re-sending a payment header is a replay and returns 402. Sign a fresh authorization per call.
- The price covers the ceiling you asked for — your
max_tokensat wholesale rates, floored at $0.01 to cover the on-chain gas we pay to collect. Unused tokens are not refunded on this rail; buy credit if you want to pay only for what you use. - The price is recomputed from the request you send. A payment signed for a small request will not buy a bigger one: you get a fresh 402 with the real price.
- $5 ceiling per authorization. Anything more expensive returns 400: buy credit.
- Chat only.
/v1/embeddingsand/v1/images/generationsstill need a key. - What we store: your wallet address and the authorization nonce, because collecting the payment on chain needs both. No key is issued, no account is created, nothing else is kept.
- Settlement is asynchronous. We serve on a verified authorization and broadcast it afterwards, so
transactionin theX-PAYMENT-RESPONSEheader is empty at response time.
// 08
Rate limits
| ENDPOINT | PER-KEY | PER-IP |
|---|---|---|
/v1/chat/completions | 60/min | 300/min |
/v1/embeddings | 60/min | 300/min |
/v1/images/generations | 30/min | 60/min |
/v1/key/rotate | 5/hour | 30/hour |
/v1/purchase | n/a | 10/min |
/v1/chat/completions via x402 | n/a | 60/min |
Hashed in memory. Never logged, never persisted. Requests with no key have no per-key bucket, so both limits land on the IP bucket, making the credential-free path the tightest one.
// 09
FAQ
▸ I lost my key.
If you have your payment ID, recover anytime at /recover.html. Your key was AES-GCM-encrypted under HKDF(payment_id) at issuance. Phantom holds ciphertext only. No payment ID → no recovery. Same SHA-256 hash of the key still gates auth.
▸ Refunds?
Crypto refunds need a return address we don't store. Buy small first.
▸ Can you see my prompts?
In flight, the proxy holds the prompt in RAM. Not logged, not persisted. Hosting provider could in principle dump memory. See trust boundaries.
▸ How long does payment take?
Hosted crypto checkout. Typical drop: ~2-20 min depending on coin and network. BTC needs ~3 confirms (~30 min). ETH ~30 confirms (~7 min). XMR ~10 confirms (~20 min). USDT/USDC on TRC-20 fastest (~1 min). Key drops on next /v1/purchase/{id}/status poll.
▸ Which coin? No crypto yet?
XMR for max privacy. BTC / ETH / USDT / USDC / LTC / SOL / DOGE if XMR isn't convenient. Phantom hides your identity either way. Non-XMR txs sit on a public ledger forever.
No crypto yet? Three-minute path: install any Monero wallet, or any multi-asset crypto wallet for BTC/ETH/USDT/USDC. Fund via any major exchange or a card-to-crypto DEX. Send to the address Phantom shows at checkout. Done.
▸ Why no /v1/usage history?
Exposing it would let attackers enumerate when a key is active. /v1/key/balance gives the only thing you need: remaining credit.
▸ How does this compare to OpenAI / Anthropic / going direct?
| vendor | account | payment | logs prompts | TEE |
|---|---|---|---|---|
| OpenAI / Anthropic / Google | required + KYC | credit card | yes (30d default) | no |
| TEE provider direct | email account | card / crypto | no | yes |
| Phantom | none | any major crypto | no | yes |
Markup: none, wholesale rate on every tier. Flat price on every coin. The no-account, no-card, no-IP layer costs you crypto friction and no refunds, not money.
▸ What happens at $0 credit?
HTTP 402 "insufficient credit or expired key". Buy a new bundle or top up.
▸ Contact / abuse?
Encrypted only. PGP key at /pgp.txt (fingerprint 09654A79076956E6042D11946296DEC4E954FC76).