# 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](/pricing/).

## 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`.

*Example: quote before you run*

**Python**

```python
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"}
```

**TypeScript**

```typescript
import Roborama from "@roborama/sdk"; // reads ROBORAMA_API_KEY

const roborama = new Roborama();

const quote = await roborama.quote({
  robot: "g1-edu-pro",
  environment: "kitchen-std",
  episodes: 600,
});
console.log(quote);
// { robot_hours: 20.0, env_hours: 20.0, usd: 3740, queue_eta: "6h" }
```

**cURL**

```bash
curl https://api.roborama.com/v1/quote \
  -H "Authorization: Bearer $ROBORAMA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "robot": "g1-edu-pro",
    "environment": "kitchen-std",
    "episodes": 600
  }'
```

**Agent (tool-use payload)**

```json
{
  "type": "tool_use",
  "name": "roborama_quote",
  "input": {
    "robot": "g1-edu-pro",
    "environment": "kitchen-std",
    "episodes": 600
  }
}
```

## 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](/docs/errors/).

## Account-level controls

*Example: usage, budgets, keys, purge*

**Python**

```python
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
```

**TypeScript**

```typescript
import Roborama from "@roborama/sdk"; // reads ROBORAMA_API_KEY

const roborama = new Roborama();

const usage = await roborama.usage({ period: "2026-09" });
console.log(usage); // robot_hours by tier, env_hours by class, $

await roborama.budgets.set({ monthly_usd: 25000, hard: true }); // hard stop

const key = await roborama.keys.create({
  scope: ["runs:write", "streams:read"],
});
console.log(key.id); // secret shown once, rbr_live_ prefix

const receipt = await roborama.data.purge("run_8842"); // customer-initiated
console.log(receipt.id, receipt.status); // receipted deletion
```

**cURL**

```bash
curl "https://api.roborama.com/v1/usage?period=2026-09" \
  -H "Authorization: Bearer $ROBORAMA_API_KEY"

curl https://api.roborama.com/v1/budgets \
  -X PUT \
  -H "Authorization: Bearer $ROBORAMA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "monthly_usd": 25000, "hard": true }'

curl https://api.roborama.com/v1/keys \
  -H "Authorization: Bearer $ROBORAMA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "scope": ["runs:write", "streams:read"] }'

curl https://api.roborama.com/v1/data/run_8842 \
  -X DELETE \
  -H "Authorization: Bearer $ROBORAMA_API_KEY"
```

**Agent (tool-use payload)**

```json
[
  {
    "type": "tool_use",
    "name": "roborama_usage",
    "input": { "period": "2026-09" }
  },
  {
    "type": "tool_use",
    "name": "roborama_set_budget",
    "input": { "monthly_usd": 25000, "hard": true }
  },
  {
    "type": "tool_use",
    "name": "roborama_create_key",
    "input": { "scope": ["runs:write", "streams:read"] }
  },
  {
    "type": "tool_use",
    "name": "roborama_purge_data",
    "input": { "run_id": "run_8842" }
  }
]
```

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()`](/docs/primitives/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.
