Open the console

SDK

Wallets and usage

Two books. Your end users' wallets hold money you credited them, which their runs spend. Your own balance is what the platform charges you. These namespaces wrap the wallets and usage APIs; Billing and the ledger explains how a run is reserved and settled.

Two units, two names

A wallet is in micro-USD — $5.00 is 5_000_000 — and its fields end in _micros. Your own balance is in tokens, and its fields end in _units. The names differ on purpose, so a figure cannot be printed with the wrong unit by accident.

  • micros("5.00") converts dollars to micro-USD from a string, an integer or (in Python) a Decimal, rounding a sub-micro remainder half up. It refuses a float, because 0.1 + 0.2 is not 0.3 in binary and a balance a thousandth of a cent out cannot be reconciled.
  • dollars(amount) goes back exactly: a Decimal in Python (Decimal('5')), a string with six decimal places in TypeScript ("5.000000"). TypeScript also has formatUsd(amount), which renders "$5.00".

Your users' wallets

A wallet belongs to one of your end users in one project, keyed by your own id for them. A run you start with end_user_id spends from it. Charging your users walks through pricing an agent and running on their behalf end to end.

Request
from elan_sdk import Elan, dollars, micros

elan = Elan.from_env()

# Safe on every sign-in: it opens a wallet or returns the one that exists.
wallet, created = elan.credit.open("ada", display_label="Ada Lovelace")

# NOT idempotent: sending this twice credits twice. ref says why, afterwards.
elan.credit.add("ada", micros=micros("5.00"), ref="welcome-credit")

wallet = elan.wallets.get("ada")
print(dollars(wallet.available_micros))          # Decimal('5')

page = elan.wallets.statement("ada", limit=25)
for entry in page.entries:                      # newest first
    print(entry.kind, entry.amount_micros, entry.ref)

Reading and moving money are two objects, so the read path cannot move money by construction: wallets issues nothing but GETs, and credit holds every write.

MethodWhat it does
wallets.list(*, limit=25, offset=0)A WalletPage: wallets and total.
wallets.get(user_id)One Wallet: status, balance_micros, held_micros, available_micros, currency, display_label and its dates.
wallets.statement(user_id, *, limit=25, offset=0)Its movements, newest first: kind, amount_micros, ref (which may be null) and created_at.
credit.open(user_id, *, display_label, currency="USD")Opens one, or returns the one that exists, with whether it was new: a tuple in Python, { wallet, created } in TypeScript.
credit.add(user_id, *, micros, ref)Adds credit and returns the Transaction. Python refuses an amount that is not positive before sending.
credit.adjust(user_id, *, micros, ref)A signed correction: negative claws back, positive refunds. Zero is refused.
credit.freeze / unfreeze / close(user_id)Stops spending without losing the balance, starts it again, or closes the wallet for good — which needs a zero balance. All three go through set_status (setStatus).
  • Reading needs any credential for the project; every credit method needs a tenant admin. Every method takes project and defaults to the client's; with neither, Python refuses locally.
  • An end user id may not be empty or contain *, : or /, and both clients refuse one before sending. * means the tenant scope, and the other two would break the account id the wallet is keyed on.
  • A credit is a transfer. Neither the service nor the SDK deduplicates one, and the SDK never retries — a timeout does not say whether the request arrived. Keep your own record of what you have credited, and use ref so a double credit is at least visible afterwards.
  • What is left to spend is derived: balance less what is held for runs in flight, which is why a balance looks lower than expected mid-run. A wallet carries available_micros; TypeScript's availableMicros(wallet) computes the same subtraction.

Your own balance and usage

usage is what has been spent, as balances and as rows a person scrolls.

Request
balance = elan.usage.balance()
print(balance.balance_units, balance.held_units, balance.available_units)   # tokens

for row in elan.usage.agents(project="salmon"):        # one row per agent version
    print(row.target_slug, row.target_version, row.holds, row.total_tokens)

charge = elan.usage.invocation(invocation_id)          # one run's cost, by model
for model in charge.usage:
    print(model.model, model.prompt_tokens, model.completion_tokens)
MethodWhat it returns
usage.balance()The tenant's own balance, in tokens: balance_units and held_units. Available is the difference — available_units in Python, availableUnits(balance) in TypeScript. The figure the service sends is kept beside it as reported_available_units (Python) and available_units (TypeScript).
usage.agents(*, project)One row per agent and version — a version bump changes what an agent costs — with holds, reserved_units, settled_units and total_tokens.
usage.holds(*, limit, offset, project, agent, minutes)The individual charges behind those rows. minutes is a window, and zero means all of it.
usage.invocation(invocation_id)What one run cost and which models spent it, plus how the run ended when that was recorded. A run with no charge — an agent that declared no prices — is a NotFound.
usage.cost(*, project, since, until, limit, offset)The platform's compute accruals behind the cost series. Python defaults to the last 30 days; TypeScript sends no window unless you give one.

A hold's units are not a token count

reserved_units and settled_units are in the unit of the account that paid: tokens for a run on your own balance, micro-USD for one a wallet funded. Charting them as tokens draws fractions of a cent for every wallet run. Token counts are total_tokens and the token series below.

Series a dashboard draws

Six calls, each returning its own shape, so a measure a series does not have is absent rather than null.

CallReturnsEach bucket carries
analytics.revenue()MoneySeriesWhat your end users paid you: micros per agent and SKU.
analytics.cost()MoneySeriesWhat the platform charged you for pods — CPU, memory, disk — per SKU. Not model tokens, which your provider bills you for.
analytics.tokens()TokenSeriesinput_tokens, cached_tokens, output_tokens, total_tokens, runs.
analytics.invocations()RunSeriesruns, ok, failed, llm_calls, tool_calls, events, tokens.
analytics.latency()LatencySeriesp50, p95, p99 in milliseconds, and runs.
analytics.tools()ToolSeriesPer tool: calls, runs, timed and that tool's own percentiles.
Request
from datetime import UTC, datetime, timedelta

since = datetime.now(UTC) - timedelta(days=7)

revenue = elan.analytics.revenue(grain="day", since=since, project="salmon")
print(revenue.total)                            # Decimal dollars, the whole window
for bucket in revenue:                          # a series iterates its buckets
    print(bucket.bucket, bucket.target_slug, bucket.sku, bucket.amount)

latency = elan.analytics.latency(grain="hour", since=since, kind="agent")
for bucket in latency:
    print(bucket.bucket, bucket.p50, bucket.p95, bucket.p99)
  • Every call takes the same window: grain (hour, day, week or month; day by default), since and until (the last 30 days by default, half-open so adjacent windows never count a bucket twice), and project. Python stamps a naive datetime as UTC. All but revenue and cost also filter by target, and invocations, latency and tools by kind (agent or workflow).
  • The three token bands stack to the total. input_tokens is already the prompt less its cached part, so input, cached and output add up to total_tokens. They come from settled charges, so a run still in flight is not in them yet.
  • Latency excludes failed runs unless you ask, because a failure's duration is time to failure, a different measurement. Python takes include_failures=True; the TypeScript includeFailures option is currently sent under a name the route does not read, so it has no effect. failures_included on the series says which you got.
  • A money series totals itself — total and total_micros in Python, totalMicros(series) in TypeScript — and each Python bucket has an amount in dollars. A deployment with no ledger or no analytics warehouse answers NotWired, which is a reason to show an empty chart, not an error.

Reading this as a model? llms.txt indexes every page, and each page has a “Copy for AI” control.