Skip to content
Runtime

How to let an AI agent test your own API in a sandbox

Run your API and the agent's tests in one microVM. Allow only your own hosts. Send each failing test back to the agent to fix or report.

On Runtime, the service under test, its tests and the agent's mistakes stay on one disposable machine. A 2 vCPU, 4 GiB sandbox costs $0.08 per hour with both CPUs busy. It costs $0.03125 per hour while the agent waits on its model. Runtime bills measured CPU use (pricing, 26 October 2026). Testing every pull request of a busy service costs about $1.28 a month. The calculation is below.

This page covers testing your own APIs. Runtime's acceptable use policy prohibits probing or vulnerability testing of any system you do not own, even with the owner's permission. The network rules below keep the agent on your own hosts.

Two ways to point an agent at an API

Setup What the agent reaches Use it for
The API runs inside the sandbox localhost only; internet off Every pull request; destructive tests are safe
Your staging API, from the sandbox Your staging host and nothing else Contract checks against the deployed service

For an agent that writes tests, running the service in the sandbox is the stronger choice. A test that deletes every record deletes only a copy, and that copy stops with the sandbox.

The short answer

Upload the service. Start it with spawn. Wait until it responds. Then run the test file the agent wrote, and return the result to the agent:

TypeScriptimport { Sandbox } from "withruntime";type Agent = (feedback: string) => Promise<string | null>; // your model: a new test file, or null when doneexport async function testApi(serviceDir: string, agent: Agent, rounds = 5) {  await using sbx = await Sandbox.create({    network: { internet: true, allow: ["pypi.org", "*.pythonhosted.org"] },    timeoutSeconds: 1800,    onLeaseEnd: "stop",  });  await sbx.files.upload(serviceDir, "/workspace/service");  await sbx.exec("pip install --quiet -r requirements.txt pytest httpx", {    cwd: "/workspace/service",    check: true,    timeoutMs: 300_000,  });  await sbx.network.set({ internet: false }); // from here on, only localhost  await sbx.spawn("python3 -m uvicorn app:app --port 8000", { cwd: "/workspace/service" });  await sbx.exec(    [      "bash",      "-c",      "for i in $(seq 60); do curl -sf localhost:8000/health && exit 0; sleep 1; done; exit 1",    ],    {      check: true,      timeoutMs: 90_000,    },  );  let feedback =    "Write pytest tests for the API at http://localhost:8000. The source is in service/.";  for (let round = 0; round < rounds; round++) {    const tests = await agent(feedback);    if (tests === null) break;    await sbx.files.write("/workspace/tests/test_api.py", tests);    const run = await sbx.exec(["python3", "-m", "pytest", "-q", "-x", "/workspace/tests"], {      cwd: "/workspace/service",      timeoutMs: 300_000,    });    feedback = `exit ${run.exitCode}${run.timedOut ? " (timed out)" : ""}\n${run.stdout.slice(-6000)}`;  }  return feedback;}
Pythonfrom withruntime import SandboxWAIT = "for i in $(seq 60); do curl -sf localhost:8000/health && exit 0; sleep 1; done; exit 1"def test_api(service_dir: str, agent, rounds: int = 5) -> str:    """agent(feedback) returns a new test file, or None when it is done."""    with Sandbox.create(network={"internet": True, "allow": ["pypi.org", "*.pythonhosted.org"]},                        timeout_seconds=1800, on_lease_end="stop") as sbx:        sbx.files.upload(service_dir, "/workspace/service")        sbx.exec("pip install --quiet -r requirements.txt pytest httpx",                 cwd="/workspace/service", check=True, timeout_ms=300_000)        sbx.network.set(internet=False)  # from here on, only localhost        sbx.spawn("python3 -m uvicorn app:app --port 8000", cwd="/workspace/service")        sbx.exec(["bash", "-c", WAIT], check=True, timeout_ms=90_000)        feedback = "Write pytest tests for the API at http://localhost:8000. The source is in service/."        for _ in range(rounds):            tests = agent(feedback)            if tests is None:                break            sbx.files.write("/workspace/tests/test_api.py", tests)            run = sbx.exec(["python3", "-m", "pytest", "-q", "-x", "/workspace/tests"],                           cwd="/workspace/service", timeout_ms=300_000)            feedback = f"exit {run.exit_code}{' (timed out)' if run.timed_out else ''}\n{run.stdout[-6000:]}"        return feedback

Replace the uvicorn line with your own start command, such as npm start or go run ./cmd/api. Replace the health check with a route your service has. The server keeps running between rounds because spawn started it. A process started by exec ends when its command ends.

Test your staging API with a key the agent never sees

To check the deployed service, allow the sandbox to reach your staging host and no other host. Store the staging token as a Runtime secret. With header, the proxy for that host sets the header on every HTTPS request to the host. The test code holds no credential.

TypeScriptimport { Runtime } from "withruntime";const runtime = new Runtime();await runtime.secrets.set("STAGING_API_TOKEN", {  value: process.env.STAGING_API_TOKEN ?? "",  hosts: ["staging-api.example.com"],  header: "Authorization",  format: "Bearer {value}",});await using sbx = await runtime.sandboxes.create({  network: { internet: true, allow: ["staging-api.example.com"] },});const probe = await sbx.exec([  "curl",  "-s",  "-o",  "/dev/null",  "-w",  "%{http_code}",  "https://staging-api.example.com/health",]);console.log(probe.stdout); // the proxy added the Authorization header

The value is sealed when you store it. No API returns it. The sandbox holds only a placeholder. So a test that prints its environment, or a prompt injection in an API response, leaks nothing usable (security). Name only hosts you own. A host that echoes requests back would show the value to the sandbox.

What an API-testing agent needs

Need How Runtime covers it
The service running beside its tests spawn keeps the server up; tests call localhost
Tests that cannot reach other hosts internet: false, or allow with only your hosts, enforced on the host
Credentials the agent cannot read Secrets added by the proxy with header and format
A test that hangs timeoutMs returns timedOut: true with the output so far
Databases and queues the API uses sudo enable-docker, then docker compose up -d in the same sandbox
One run per pull request, in parallel 100 sandboxes at the same time on a paid account
A record of what ran stdout, stderr and exit code per command; files read back with files.read

For a service that needs Postgres or Redis, see Docker in a sandbox: Compose services reach each other by name inside the sandbox, as they do anywhere.

What it costs

Assume 300 pull requests a month. Each run keeps a 2 vCPU, 4 GiB sandbox running for 6 minutes, including the install and the agent rounds. On average, the sandbox uses 0.5 of a vCPU:

CPU
300 × 6 min / 60 × 0.5 vCPU × $0.025
$0.38
Memory
300 × 6 min / 60 × 4 GiB × $0.0075
$0.90
Total
$1.28

Put the install in a custom image so each run skips it. Your model provider bills the model tokens. Every account gets 100 free hours of a 1 GB machine in its first 30 days, with no card. Without credit, a sandbox reaches ports 80 and 443.

Start

Terminalprintf '%s\n' 'print(sum([125, 250, 375]))' > invoice.pynpx withruntime sandboxes run -- python3 - < invoice.py

The first run prints a link. Open it in your browser to approve the run. Then use the JavaScript or Python SDK as shown above.

Related: end-to-end testing in sandboxes, egress control, a coding agent sandbox, run Docker in a sandbox.

Facts on this page were checked on 26 October 2026.

Related.

100 free hoursin your first 30 days.

of a 1 GB machine

  • No credit card
  • Four sandboxes at once, 2 vCPU and 4 GiB each
  • Then prepaid credit from $10, no plan fee
Start free, no card
Start free, no card