> ## Documentation Index
> Fetch the complete documentation index at: https://docs.costgraph.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Use AutoPilot for sandboxes from any language

> Choose a provider for each new sandbox and report sandboxes starting and stopping with two REST calls, from Python or any other language.

[AutoPilot for sandboxes](https://docs.costgraph.ai/costgraph/autopilot/sandboxes.md) works through two
REST endpoints. The TypeScript package `@costgraph/computesdk` calls them for
you. From Python or any other language, call them directly:

1. Before you create a sandbox, ask CostGraph which of your providers to try
   first with `POST /api/v1/tenant/sandboxes/route`.
2. When a sandbox starts or stops, report it with
   `POST /api/v1/tenant/sandboxes/events`.

You can report sandboxes without routing them. CostGraph then tracks what they
cost, and they stay on the providers you chose.

## Requirements

* A CostGraph API key with the `sandbox:route` scope. Create it in
  **Settings > Personal API Keys**.
* Your provider names as ComputeSDK spells them, such as `e2b`, `daytona`,
  `modal`, and `vercel`. CostGraph prices providers by these names.

Send the key in the `X-API-Key` header. The base URL is
`https://api.costgraph.ai`. To act for a specific tenant, add the
`X-CostGraph-Tenant-ID` header. Responses wrap their payload in `data`. Refer to
the [CostGraph API introduction](https://docs.costgraph.ai/api-reference/costgraph/introduction.md) for
authentication and tenants.

## Choose a provider

Send the size of the new sandbox and your providers in your preferred order.

```shell theme={null}
curl https://api.costgraph.ai/api/v1/tenant/sandboxes/route \
  -H "X-API-Key: $COSTGRAPH_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"size_class": "md", "providers": ["e2b", "daytona"]}'
```

The request takes the following fields.

| Field | Description |
| - | - |
| `providers` | Required. Your provider names in your order. The first is the baseline. A repeated name counts once, and only the first 20 are priced. |
| `size_class` | The sandbox size: `xs`, `sm`, `md`, `lg`, `xl`, or `2xl`. Refer to [Sizes](https://docs.costgraph.ai/costgraph/autopilot/sandboxes.md#sizes). |
| `vcpus`, `memory_mib` | The sandbox size as vCPUs and memory in MiB, for a size that is not a standard size. `size_class` wins when you send both. |

The response lists the order to try your providers in.

```json theme={null}
{
  "data": {
    "decision_id": "6f1c2a9e-0b7d-4c41-9a53-2f7f4f0f9b11",
    "provider": "daytona",
    "order": ["daytona", "e2b"],
    "reason": "cheapest",
    "expected_seconds": 600,
    "candidates": [
      {
        "provider": "daytona",
        "priced": true,
        "price_per_hour": 0.0828,
        "expected_cost": 0.0138,
        "startup_p50_ms": 412,
        "success_rate": 1
      },
      {
        "provider": "e2b",
        "priced": true,
        "price_per_hour": 0.1656,
        "expected_cost": 0.0276,
        "startup_p50_ms": 380,
        "success_rate": 1
      }
    ]
  }
}
```

The numbers in this example are illustrative. The response has the following
fields.

| Field | Description |
| - | - |
| `decision_id` | The ID of this answer. Send it back with the sandbox's `started` event. It is empty when CostGraph could not choose. |
| `provider` | The provider to try first. |
| `order` | Every provider you sent, in the order to try them. |
| `reason` | Why the order is what it is. Refer to [Reasons](#reasons). |
| `expected_seconds` | The run time used to compute expected cost. |
| `candidates` | Each provider in the returned order with its price and benchmark figures. It is empty when CostGraph could not choose. |
| `billing_url` | A link to your billing settings, sent when the reason is `billing_required`. |

Each candidate has the following fields. A figure that is unknown is `null` or
left out.

| Field | Description |
| - | - |
| `provider` | The provider name. |
| `priced` | Whether CostGraph has a list price for this provider at this size. |
| `price_per_hour` | The list price per hour for the size, in USD. |
| `expected_cost` | The cost in USD of one sandbox of this size running for `expected_seconds`, rounded up to the provider's billing increment and at least its minimum billed time. |
| `startup_p50_ms` | The median milliseconds from a create request to the first command, from ComputeSDK's public burst benchmark of 100 sandboxes started at once. |
| `success_rate` | The share of the benchmark's 100 starts that succeeded, from 0 to 1. |

The guide explains [expected cost and start time](https://docs.costgraph.ai/costgraph/autopilot/sandboxes.md#expected-cost-and-start-time).

### Order within each policy

When AutoPilot is on, the order follows your
[routing policy](https://docs.costgraph.ai/costgraph/autopilot/sandboxes.md#policies). The baseline is the
first provider you sent.

* **`cheapest`** puts providers with a price for the size first, lowest
  `expected_cost` first, then the others. A provider whose `startup_p50_ms` is
  more than twice the baseline's goes after the other priced providers. A
  missing start time never holds a provider back.
* **`fastest`** puts providers whose `expected_cost` is no more than the
  baseline's first, fastest start first, then lower `expected_cost`. Providers
  without a start time follow them by `expected_cost`. Providers that cost more
  come next in the same way, then providers without a price. The baseline stays
  first unless a provider starts strictly faster.
* **`balanced`** sets a limit at the baseline's start time plus 100ms or 10%,
  whichever is larger. A provider whose `expected_cost` is no more than the
  baseline's and whose start time is within the limit scores the baseline's
  `expected_cost` minus its own, multiplied by the limit minus its
  `startup_p50_ms`. Providers that score above zero come first, highest score
  first, then faster start, then lower `expected_cost`. The other priced
  providers follow by start time, with those without one last, then by
  `expected_cost`. Providers without a price come last. The baseline stays first
  when no provider scores above zero.

In every policy, a provider other than the baseline whose `success_rate` is
below 0.95 moves to the end. Remaining ties keep your order.

### Reasons

`reason` takes one of the following values.

| Reason | Meaning |
| - | - |
| `cheapest`, `balanced`, `fastest` | The policy moved another provider ahead of the baseline. |
| `no_better_candidate` | No provider fits the policy better than the baseline. |
| `off` | AutoPilot for sandboxes is off. The order is yours. |
| `no_size` | The request had no size. The order is yours. |
| `baseline_not_priced` | Your first provider has no list price for the size. The order is yours. |
| `billing_required` | Your organization's billing needs attention. The order is yours, and `billing_url` links to your billing settings. |
| `rate_limited`, `error` | CostGraph could not choose this time. The order is yours. |

While AutoPilot is off, CostGraph still records each answer, so the
**Sandboxes** tab can show what routing would change.

### Use a short timeout and fall back

The route endpoint answers `200` with your own order whenever it cannot choose.
It answers `400` only when `providers` is empty or the body is not valid JSON,
and `403` when the key lacks the `sandbox:route` scope.

Don't make a sandbox wait on CostGraph. Give the call a short timeout, such as
300ms. If the call times out, fails, or returns an error status, create the
sandbox in your own provider order. Try the providers in `order` until one
creates the sandbox.

To avoid a call before every sandbox, reuse an answer for a few minutes for the
same size and provider list. A changed setting reaches your sandboxes when your
reused answer expires.

## Report sandboxes starting and stopping

Send up to 100 events in one call.

```shell theme={null}
curl https://api.costgraph.ai/api/v1/tenant/sandboxes/events \
  -H "X-API-Key: $COSTGRAPH_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "events": [
      {
        "type": "started",
        "provider": "daytona",
        "sandbox_id": "sbx_123",
        "at": "2026-11-05T12:00:00Z",
        "decision_id": "6f1c2a9e-0b7d-4c41-9a53-2f7f4f0f9b11",
        "size_class": "md",
        "metadata": { "agent": "researcher", "run": "run-42", "customer": "acme" }
      },
      { "type": "stopped", "provider": "daytona", "sandbox_id": "sbx_123", "at": "2026-11-05T12:07:30Z" }
    ]
  }'
```

The response gives the number of events recorded, as in
`{"data": {"accepted": 2}}`. Each event takes the following fields.

| Field | Description |
| - | - |
| `type` | Required. `started` or `stopped`. |
| `provider` | Required. The provider name. |
| `sandbox_id` | Required. The provider's sandbox ID, up to 256 characters. |
| `at` | When the sandbox started or stopped, in RFC 3339 format. It defaults to the time CostGraph receives the event. |
| `decision_id` | The `decision_id` from the routing answer. Send it with `started`. |
| `size_class` | The sandbox size on `started`: `xs`, `sm`, `md`, `lg`, `xl`, or `2xl`. |
| `vcpus`, `memory_mib` | The sandbox size on `started`, for a size that is not a standard size. |
| `metadata` | Tags for the sandbox on `started`, such as `agent`, `run`, and `customer`. At most 20 string values. Keys can be up to 64 characters and values up to 256 characters. |

CostGraph records events with the following rules:

* **A `started` event is priced at the list price for its size at that moment.**
  Without a size in the event, CostGraph uses the size from its `decision_id`.
  With neither, it uses the provider's documented default size when the price
  list gives one.
* **A sandbox started where AutoPilot sent it counts toward savings.** This
  applies when the answer moved the sandbox off your first provider and it
  started there at the routed size. CostGraph ignores an unknown `decision_id`.
* **Repeats are ignored.** CostGraph ignores a second `started` or a second
  `stopped` for the same sandbox.
* **A stop can arrive before its start.** CostGraph keeps it for up to 2 days
  until the start arrives. A stop is never recorded before its start.
* **Times are kept within the last 24 hours.** An `at` older than 24 hours
  counts as 24 hours ago. An `at` in the future counts as the time CostGraph
  receives the event.
* **One invalid event rejects the whole request** with `400`. Keys and values in
  `metadata` cannot contain control characters.
* **Too many requests answer `429`.** Send events in batches of up to 100 and
  retry shortly.

CostGraph records events even while your organization's billing needs
attention.

Report a `stopped` event for every sandbox you end. A sandbox without one shows
as running, and its estimated cost grows until the 24-hour cap.

## Python example

The following example uses the `requests` library. `create_sandbox` stands for
your own code. It tries each provider in the order it receives and returns the
provider that created the sandbox and the sandbox's ID.

```python theme={null}
import os
from datetime import datetime, timezone

import requests

API = "https://api.costgraph.ai/api/v1/tenant/sandboxes"
HEADERS = {"X-API-Key": os.environ["COSTGRAPH_API_KEY"]}
PROVIDERS = ["e2b", "daytona"]


def choose_order(size_class: str) -> tuple[list[str], str]:
    try:
        response = requests.post(
            f"{API}/route",
            headers=HEADERS,
            json={"size_class": size_class, "providers": PROVIDERS},
            timeout=0.3,
        )
        response.raise_for_status()
        data = response.json()["data"]
        return data["order"], data["decision_id"]
    except (requests.RequestException, KeyError, ValueError):
        return PROVIDERS, ""


def report(event: dict) -> None:
    event["at"] = datetime.now(timezone.utc).isoformat()
    try:
        requests.post(f"{API}/events", headers=HEADERS, json={"events": [event]}, timeout=5)
    except requests.RequestException:
        pass


order, decision_id = choose_order("md")
provider, sandbox_id = create_sandbox(order, size_class="md")
started = {
    "type": "started",
    "provider": provider,
    "sandbox_id": sandbox_id,
    "size_class": "md",
    "metadata": {"agent": "researcher", "run": "run-42"},
}
if decision_id:
    started["decision_id"] = decision_id
report(started)

report({"type": "stopped", "provider": provider, "sandbox_id": sandbox_id})
```

Call the final `report` when you destroy the sandbox. In a busy service, queue
events and send them in batches of up to 100 so reports don't slow your
sandbox calls.

## Read settings and results

The AutoPilot page reads the following endpoints. You can call them too, with
an API key that has the `focus:read` scope or as a signed-in member.

| Endpoint | Returns |
| - | - |
| `GET /api/v1/tenant/sandboxes/autopilot/settings` | Whether AutoPilot for sandboxes is on, and its `policy`. |
| `GET /api/v1/tenant/sandboxes/autopilot/summary` | The totals, cost by provider, and the 50 tags with the highest cost for the period. |
| `GET /api/v1/tenant/sandboxes/autopilot/runs` | Every sandbox in the period, newest first. Filter with `provider`, and page with `limit` of up to 200 and `offset`. |

`summary` and `runs` take `from` and `to` as `YYYY-MM-DD`, where `to` is the day
after the last day. The default period is the last 30 days, and a period over
400 days is rejected.

Changing settings with `PUT` on the settings path is for owners and admins
signed in to CostGraph. Send `enabled` and, optionally, `policy`. Without
`policy`, the current policy is kept.

```json theme={null}
{"enabled": true, "policy": "balanced"}
```


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.