Documentation menu

Billing & budgets

Two per-minute meters, a quote before every job, and hard stops at both the run and the account level.

Everything you buy here reduces to two meters: robot-hours (occupancy of an embodiment) and environment-hours (occupancy of a scene), metered per minute. A queue priority multiplies both — burst ×1.5, standard ×1.0, soak ×0.6. Both meters appear in every quote and every result, so a bill is always reconstructible from numbers you already saw. The full rate card is on the pricing page.

Quote first

A quote resolves both meters, the dollars, and the queue before you commit anything. The worked example: 600 episodes of g1-edu-pro in kitchen-std@v1.2 at standard priority estimates 2.0 minutes per episode, so 20.0 robot-hours × $148 plus 20.0 environment-hours × $39 — $3,740, queue ETA about 6 hours. Quotes expire after seven days; a stale one is refused with quote_expired.

quote before you run
import roborama  # reads ROBORAMA_API_KEY from the environment

quote = roborama.quote(
    robot="g1-edu-pro",
    environment="kitchen-std",
    episodes=600,
)
print(quote)
# {robot_hours: 20.0, env_hours: 20.0, usd: 3740, queue_eta: "6h"}

The run-level hard stop

Every run accepts max_budget_usd, metered live against both meters. A run that hits the cap stops with budget_exceeded and keeps the partial result — n, success rate, and ci95 for the episodes that did run. A run stopped at episode 81 still tells you things; it just tells you them with a wider interval. Codes and envelope shapes are catalogued at the error reference.

Account-level controls

usage, budgets, keys, purge
import roborama  # reads ROBORAMA_API_KEY from the environment

usage = roborama.usage(period="2026-09")
print(usage)   # robot_hours by tier, env_hours by class, $

roborama.budgets.set(monthly_usd=25_000, hard=True)  # hard stop, metered

key = roborama.keys.create(scope=["runs:write", "streams:read"])
print(key.id)  # secret shown once, rbr_live_ prefix

receipt = roborama.data.purge("run_8842")   # customer-initiated deletion
print(receipt.id, receipt.status)           # receipted

Four controls, one per line of that example:

  • roborama.usage(period="2026-09") — robot-hours by tier, environment-hours by class, and dollars, mid-month. These are the same numbers the invoice will use.
  • roborama.budgets.set(monthly_usd=25_000, hard=True) — an account-wide hard stop. Once spend crosses the cap, new runs are refused with monthly_budget_exceeded until the period rolls over.
  • roborama.keys.create(scope=["runs:write", "streams:read"]) — scoped keys, least privilege. A CI key needs runs:write and nothing else; a triage dashboard needs streams:read; keep data:purge on a human-held key. A call outside a key's scope fails with insufficient_scope, which is the point.
  • roborama.data.purge(run_id) — customer-initiated deletion. The response is a receipt with an id and status, so the deletion is a fact you can file, not a support ticket you can hope about.

Spending less on purpose

The tiers are the tool. Iterate on the soak tier — nori-a3 pods at $7/robot-hour, or soak priority at ×0.6 on any embodiment — and reserve verification-tier hardware for the runs that produce reports someone will cite. If managing that ladder sounds like a job for a machine, threshold() is that machine: it iterates on soak, escalates to the verification tier only when the target is crossed, and takes a monthly_cap_usd of its own.