Use Sandboxes from Python or TypeScript
A small helper for each language that creates a sandbox, runs commands, moves files, forks it, and deletes it. No SDK to install.
The Sandbox API is plain HTTPS with a bearer key, so one short file is enough: the Python helper uses only the standard library, and the TypeScript one only fetch. Copy one into your project and replace the demo at its end with your own steps. From a shell, the CLI does the same.
1. Export an API key
Make a key under Dashboard → Settings → API keys with Full access, or Custom with Sandboxes: Write (Open Sandboxes also makes one). The helpers read it from PRIZED_API_KEY; never paste a key into code.
export PRIZED_API_KEY=dcp_…2. Save a helper
import json, os, time, urllib.error, urllib.parse, urllib.request
API = "https://api.prized.dev/api/v1"
KEY = os.environ["PRIZED_API_KEY"] # never hardcode a key
FREE = "That is a free-tier limit (https://prized.dev/docs/sandboxes#free-tier); a top-up or a plan lifts it."
def call(method, url, body=None, data=None):
headers = {"Authorization": f"Bearer {KEY}", "Content-Type": "application/octet-stream"}
if body is not None:
data, headers["Content-Type"] = json.dumps(body).encode(), "application/json"
try:
with urllib.request.urlopen(urllib.request.Request(url, data, headers, method=method), timeout=3660) as r:
out = r.read()
return json.loads(out) if r.headers.get_content_type() == "application/json" else out
except urllib.error.HTTPError as e:
out = e.read()
err = json.loads(out) if out.startswith(b"{") else {"error": out[:200]}
hint = FREE if err.get("tier") == "free" else ""
e.msg = f"{err} {hint}" # str(e) is now "HTTP Error 429: {'error': ...} That is a free-tier limit ..."
raise
class Sandbox:
def __init__(self, id):
self.id = id
@classmethod
def create(cls, template="prized-base-v1"):
body = {"template": template, "resources": {"vcpu": 2, "memoryMb": 4096}, "maxRuntimeMin": 60}
return cls(call("POST", f"{API}/sandboxes", body)["sandbox"]["id"])
def wait(self): # until running and taking commands
while (sbx := call("GET", f"{API}/sandboxes/{self.id}")["sandbox"])["state"] == "starting":
time.sleep(1)
if sbx["state"] != "running":
raise RuntimeError(f"{self.id} is {sbx['state']}: {sbx['errorReason']}")
self.toolbox = sbx["toolboxBase"]
self.exec("true", retry=(409, 429, 502, 503)) # a no-op, so retrying even a 502 is safe
return self
def exec(self, command, retry=(409, 429), **opts): # opts: cwd, env, timeout
for i in range(6):
try:
return call("POST", f"{self.toolbox}/process/execute", {"command": command, **opts})
except urllib.error.HTTPError as e: # 409 and 429 come before the command runs
if e.code not in retry or i == 5:
raise
time.sleep(i + 1)
def write(self, path, data: bytes):
call("POST", f"{self.toolbox}/files/upload?path={urllib.parse.quote(path)}", data=data)
def read(self, path) -> bytes:
return call("GET", f"{self.toolbox}/files/download?path={urllib.parse.quote(path)}")
def fork(self, count):
out = call("POST", f"{API}/sandboxes/{self.id}/fork", {"count": count})
return [Sandbox(s["id"]) for s in out["sandboxes"]]
def delete(self):
call("DELETE", f"{API}/sandboxes/{self.id}")
sbx, kids = Sandbox.create(), []
try:
print(sbx.wait().exec("python3 --version")["stdout"], end="")
sbx.write("/home/prized/work/hello.txt", b"hello from Python\n")
print(sbx.read("/home/prized/work/hello.txt").decode(), end="")
kids = sbx.fork(2) # two live copies, hello.txt included
for kid in kids:
print(kid.id, kid.wait().exec("cat hello.txt")["stdout"], end="")
finally:
for s in [sbx, *kids]:
try:
s.delete()
print("deleted", s.id)
except Exception as e: # try the rest; maxRuntimeMin removes this one anyway
print("could not delete", s.id, e)3. Run it
The demo creates a 2 vCPU, 4 GB sandbox that deletes itself after 60 minutes, runs a command, writes and reads a file, forks two copies that already have the file, and deletes all three. Python needs 3.8 or later:
python3 sandbox.pyNode 22.18 or later runs the TypeScript file as it is; on Node 18 to 22.17, run npx tsx sandbox.mts instead. Older Node has no global fetch, so upgrade it first.
node sandbox.mtsA run of the Python file takes about 8 seconds:
Python 3.12.3
hello from Python
sbx_2f7f62ea3f77116f hello from Python
sbx_0426aba73aa4e2ff hello from Python
deleted sbx_723373199c11b589
deleted sbx_2f7f62ea3f77116f
deleted sbx_0426aba73aa4e2ffThe TypeScript file prints the same with v22.23.3 and hello from TypeScript.
What the helper handles
- Waiting. A create answers while the sandbox is
starting.wait()reads it untilrunning, keeps itstoolboxBase(where its commands and files go), then runstrueuntil the sandbox takes commands: for a moment afterrunning, it can still answer409or502. - Retries.
exec()retries409 sandbox_not_runningand429 rate_limited, which come before a command runs, up to 5 times. It never retries502 sandbox_unreachable, since the command may have run; for one that is safe to run twice, passretry=(409, 429, 502)in Python or[409, 429, 502]as the third argument in TypeScript. - Cleanup.
finallydeletes the sandbox and each fork even when a step or another delete fails, andmaxRuntimeMin: 60deletes anything left behind. - Files.
read()returns bytes, so archives and images arrive intact; decode text yourself, as the demo does. - Long commands. A command runs for up to 300 seconds unless you pass
timeout(3,600 at most). Node'sfetchstops waiting for an answer after 300 seconds, so in TypeScript run a job that may take that long in the background, such asnohup ./job.sh > job.log 2>&1 &, and readjob.logwith later commands.
Errors
An error carries the API's status and body: the error code, and fields such as reason or fits. When a free-tier limit refused the request, the body has "tier": "free" and the helper adds a link to the limits. The ones you are most likely to see (every code):
| Error | Means | Do |
|---|---|---|
429 sandbox_quota_exceeded | A quota or free-tier limit, named in reason. Paused sandboxes count | Delete sandboxes you are done with; on the free tier, a top-up or a plan lifts the limits |
422 invalid_resources | A shape outside the template's bounds, or the free tier's 2 vCPU and 4 GB | Ask for less |
409 fork_capacity | The parent's machine has room for only fits more copies | Fork fits or fewer; at fits: 0, fork a new sandbox instead, or wait |