PHANTOM AI
Buy a key

Concepts

This page explains the parts of Phantom AI that the API reference does not.


Child keys

A key can create a child key that spends from its balance, up to a limit. Give each subagent or task its own child key, and it can only spend up to its limit.

curl -X POST https://phantom.codes/v1/key/child \
  -H "Authorization: Bearer $PHANTOM_KEY" \
  -H "Content-Type: application/json" \
  -d '{"limit_usd": 0.50, "ttl_hours": 6, "rate_usd_per_min": 0.10}'

No credit moves. Each call the child makes is paid from the parent's balance and counts against the child's limit, its per-minute cap, and the parent's own caps. A child key can call every model endpoint, but can't create child keys. It expires after ttl_hours, 24 by default, and never after its parent. It stops when the parent is deleted. The key is shown once and Phantom AI stores only its hash, so save it when you get it.

Two caps limit spending.

CapSet withLimits
budget_usdPATCH /v1/key/budgetSpending in one period: a month, or period_days days.
rate_usd_per_minPATCH /v1/key/budgetSpending in one minute. This stops a loop that keeps calling the API.

A child key takes a per-minute cap and a limit_usd over its life when you create it. Caps are checked before every paid call. Over the budget, the call returns 402. Over the per-minute cap, it returns 429 with a Retry-After header. Rotating a key keeps both caps and what has been spent against them.

Managing keys

RouteDoes
POST /v1/key/rotateIssues a new key and disables the old one. The balance, expiry, caps, amounts spent and child keys all move to the new key.
DELETE /v1/keyDeletes the calling key, and stops its child keys. Calling it again does nothing. Refused with 409 while a request on the key is running.

Keys with no parent

A key you bought or got from the free grant has no parent. If you delete it, its leftover credit is lost. Phantom AI would need to know who owns the key to send the credit anywhere else.

Example

An agent with a $5 key creates a child key with a $0.50 limit for each task, a $0.10 per-minute cap and a six-hour expiry. The worker uses the child key and cannot spend more than $0.50 of the parent's credit. When the task ends, the worker deletes its key. Nothing was moved, so nothing is left over. No account is involved.


Plans and routing

A plan is a budget with a length. {"budget_usd": 20, "period_days": 30} on PATCH /v1/key/budget allows $20 over 30 days, then starts again. Without period_days the period is one month. Unspent credit stays on the key.

GET /v1/key/budget shows the pace: what an even spend would have used by now, what is left per day, and whether the key is on_pace or ahead. What a child key spends counts against the parent's plan.

A route policy picks the model when a request asks for model: "auto". It lists up to 10 models and a list of rules. The first rule that matches picks the model. If none match, the first model runs.

curl -X PUT https://phantom.codes/v1/key/route \
  -H "Authorization: Bearer $PHANTOM_KEY" \
  -H "Content-Type: application/json" \
  -d '{"models": ["anthropic/claude-sonnet-4.5", "deepseek/deepseek-v3.2"],
       "rules": [{"if": {"pace": "ahead"}, "use": "cheapest"},
                 {"if": {"budget_left_pct_below": 20}, "use": "deepseek/deepseek-v3.2"}],
       "on_empty": "cheapest"}'

A named model runs as asked, unless the policy sets applies_to: "all". With on_empty: "cheapest", an auto request keeps running on the cheapest model after the budget runs out, instead of returning 402. With fallback_on_error, the other models are tried if the chosen one fails. The response header x-phantom-routed names the model and the rule. The request header x-phantom-route: off turns routing off for one call. POST /v1/key/route/test shows what a request would run on, without running it. Child keys get their parent's policy unless they are given one.

A conversation stays on the model it started on for stick_minutes (default 5) after its last request, so its prompt cache stays warm. It moves mid-conversation only when the new model is cheaper than staying at the cached rate; otherwise the header says reason=cache. Send x-phantom-session with any value to mark a conversation. Without it, the conversation is recognised by a hash of its opening messages, kept in server memory until it expires and never written to the database. stick_by_prompt: false turns the hash off, and stick_minutes: 0 turns keeping off.


Receipts

Every paid response includes a signed receipt. It states which model answered, how many tokens it used and what it cost.

x-phantom-receipt: <base64url payload>.<base64url signature>

A stream sends the receipt as its last SSE event, event: phantom.receipt, just before data: [DONE]. The receipt lists the model requested, the model served, prompt, completion and reasoning tokens, the cost in micro-USD, and SHA-256 hashes of the request and the response. It contains no prompt or completion text.

The signature is Ed25519 over the payload. /v1/receipts/key serves the public key as JWK and PEM, so you can check a receipt yourself.

const [payload, signature] = receipt.split('.');
const { public_key_jwk } = await fetch(
  'https://phantom.codes/v1/receipts/key',
).then((r) => r.json());

const key = await crypto.subtle.importKey(
  'jwk', public_key_jwk, { name: 'Ed25519' }, false, ['verify'],
);

const ok = await crypto.subtle.verify(
  { name: 'Ed25519' }, key, b64url(signature), b64url(payload),
);

// If ok is true, read the receipt.
const claim = JSON.parse(new TextDecoder().decode(b64url(payload)));
claim.model_served === claim.model_requested;

/verify runs this check in your browser. It does not ask a Phantom AI endpoint whether the receipt is valid.

Recording a receipt on Solana

The receipt hash is the SHA-256 of the full receipt string. POST /v1/receipts/anchor writes that hash to Solana for $0.01, which records when the receipt existed. Recording the same receipt again is free. Anyone can look it up with GET /v1/receipts/anchor?leaf= and the receipt hash, with no key. Recording is optional.

What a valid signature does not prove

A valid signature shows that Phantom AI signed this receipt and that it has not been changed. For chat, embeddings and rerank, model_served is the model name in the provider's response. For other endpoints it is the model you requested. So a receipt shows what was reported. It cannot show that the provider told the truth. You still rely on the Vercel AI Gateway for that.

Receipts are not stored. Phantom AI signs each one, sends it to you and keeps no copy, so it holds nothing to match an input_hash against. Save any receipt you want to check later.


What is linked to a key

The api_keys table has no column for a person, and no code joins a key hash to a user. A key bought with crypto is not linked to a person anywhere in the database.

Two tables can link a key to you.

TableWritten when
account_grantsYou claim the free grant. It stores your user id with the key hash it funded, so each person gets the grant once.
payments.target_key_hashYou buy credit for a key you already have. Empty otherwise.

GET /v1/privacy checks your key. identity_linkable and payment_linkable say whether either link exists for it, from a live count of rows. The invariants block lists both links as present in the schema.

curl https://phantom.codes/v1/privacy \
  -H "Authorization: Bearer $PHANTOM_KEY"

However you bought the key, the API does not store prompt or completion text. usage_log holds a key hash, a model id, token counts and a cost. /v1/privacy returns usage_rows, the number of those rows for your key.


The API reference lists every endpoint with a curl example.