Every SDK request costs credits, and every credit movement is written down. This page is the whole accounting model.
What a credit is#
One credit is $0.001 of upstream cost. Trace settles against the cost the
provider reports, multiplied by the deployment-wide multiplier shown by
GET /models (1.0 on the hosted app). See Models and pricing.
Getting credits#
| Source | Amount |
|---|---|
| Free plan | 2,000 credits ($2), renewed monthly |
| Self-learner plan | 5,000 credits ($5), renewed monthly |
| Teacher or administrator grant | any amount, on request |
Monthly allowances reset at the start of the billing period and do not roll over. If you run out, wait for the next reset, upgrade, or ask your teacher if you use Trace through a class.
Reserve, then settle#
Trace cannot know what a request will cost until the model has answered, so it charges in two steps.
- Reserve. Before the call, Trace estimates the worst case — your prompt
plus the full
max_tokensyou allowed, at the model's rate — and takes that from your balance. If your balance will not cover it, the request is refused with a 402 and no ledger row is written. - Settle. After the call, Trace uses the provider's reported cost. The settlement returns unused credits or charges an underestimated difference.
A single request therefore leaves two ledger rows sharing one request_id:
| kind | delta | meaning |
|---|---|---|
spend | negative | the reservation |
settlement | positive or negative | reservation minus provider cost |
Add the two together and you have the true cost, which is also the number on
completion.credits_spent.
Two consequences worth knowing:
- A large
max_tokensreserves a large amount, even if the reply is short. The excess comes straight back, but during the call your available balance is lower. If a request is refused with a 402 and you think you can afford it, lowermax_tokens. - Missing usage keeps the reservation. If a connection drops before Trace receives authoritative cost, it does not guess or refund consumed output.
Reading your balance and ledger#
from trace_sdk import Trace
client = Trace()
print(client.credits.balance())
for entry in client.credits.usage(limit=10):
print(entry.created_at, entry.kind, entry.delta, entry.model)
trace credits
trace credits --usage 20
Each entry carries:
entry.kind # "grant" | "spend" | "settlement" | "refund" | "admin_adjust"
entry.delta # signed change in credits
entry.balance_after # the balance immediately after this row
entry.model
entry.request_id # pairs the spend with its settlement
entry.tokens_in # on the settling row
entry.tokens_out
entry.note
The ledger is a complete explanation of your balance: replay the deltas in order
and you land on the number client.credits.balance() returns. The backend asserts this in a
test, and the balance is never written without its ledger row in the same
database transaction.
The same table is on your account page at /account/developer.
These limits are also part of the child-protection pipeline described in Authentication: the credit balance must cover the reservation before Trace contacts the provider. If the final provider cost exceeds that estimate, future calls stop until the negative difference is covered.
Limits#
| Limit | Value | What you get if you cross it |
|---|---|---|
| Requests per key | 30 per minute | RateLimitError (429) with retry_after |
max_tokens per request | 4000 | BadRequestError (400) naming the cap |
| Request body size | 256 KB | 413, code: "request_too_large" |
| Texts per embeddings request | 128 | 400, code: "too_many_inputs" |
| Answers per call | 1 | 400 if you pass n > 1 |
The rate limit is per key, so a runaway loop in one script does not lock you out of another. If you are looping deliberately, put a short sleep between iterations rather than catching the 429.
Estimating before you spend#
rate = client.models.retrieve("deepseek/deepseek-v4-flash-0731:nitro").credits_per_1k_tokens
prompt_characters = 1200
estimated_tokens = prompt_characters / 4 + 300 # prompt + expected answer
print(f"about {estimated_tokens / 1000 * rate:.4f} credits")
The published rate is for estimating reservations. Final charges use provider cost.
Handling an empty balance#
from trace_sdk import InsufficientCreditsError
try:
completion = client.chat.completions.create(
messages=[{"role": "user", "content": "Summarise this chapter."}],
)
except InsufficientCreditsError as error:
print(f"Balance {error.balance}, this needs {error.estimated_cost}")
The exception carries both numbers, so a program can decide to retry with a
smaller max_tokens or a cheaper model rather than just stopping.
Not covered by credits#
The Trace agent builder has its own per-run cost caps and does not draw on your credit balance. The two systems are separate in this version; unifying them is a planned later change. Building an agent visually costs you nothing from this balance.
Development mode#
A Trace deployment running with MOCK_OPENROUTER_ENABLED=true — which is the
default in the local docker compose stack — answers from a built-in echo model
instead of calling a provider. Replies begin with [mock], embeddings are
deterministic vectors derived from the text, and nothing leaves the machine.
Credits still work exactly as described above: reservations, settlements and
ledger rows are all real, so you can develop and test the whole billing path
without an upstream key. GET /models reports upstream_mode: "mock" so a
program can tell which it is talking to.
This mode is for local development only. A deployment serving real students runs
with a provider key and MOCK_OPENROUTER_ENABLED=false.