Start here#
agi.diy serves robot policies over an API. You send camera images and joint state; we return the next two seconds of motion. Your code runs the control loop. SO-101 only today.
One listed model: agi-wfm/so101-ttt, a world model post-tuned to play tic-tac-toe — the flagship from here on. The run on the landing page is a real session: the x piece placed on the bottom right cell in 11.0 s over 7 calls of 744–939 ms, about 690 ms of each in the policy. In simulation it succeeds 117 times in 180 (65 %); nobody has measured it on a real arm yet.
Two ways in: play the game in a simulated SO-101 (no install; the calls and charges are real), or run it on your own robot, below.
Your first call#
Python 3.12 (agi rollout does not run on 3.14). One call, synthetic inputs, nothing moves, 32 actions = $0.0032:
python3.12 -m venv .venv && source .venv/bin/activate && pip install agi-diy
agi login # paste a key from /dashboard — shown once
agi run agi-wfm/so101-ttt --probe -n 1 # black frames + a jittered zero state
What comes back:
chunk 1/1: shape (32, 6) dtype float32 · 1052 ms · first [22.35, -45.13, 11.58]…
billed 32 tokens (32 actions per call, 1 token each) · `agi usage`
trace https://api.agi.diy/dashboard/sessions/ses_…
32 rows × 6 joints at 15 Hz is 2.13 s of motion. Each row is one target per joint (shoulder_pan … gripper) in normalized units (arm −100…100, gripper 0…100) — never degrees.
The loop#
A session leases one running copy of the policy. Create it, connect within 60 s, then repeat: send front + wrist frames and the six-value state, get a chunk, execute it. Idle costs nothing; every returned action costs one token, executed or not.
Do not wait for a chunk to finish: send the next observation while motion is still queued, with the row you have reached (cursor), and the new chunk is spliced in where the arm is.
Your latency budget is the refill, not the chunk. agi rollout asks for the next chunk when 0.75 s of motion is left, so a call has about 750 ms whatever the horizon. If the link drops (LeRobot @d3793b7e): the arm finishes the queued chunk and holds; after 3 s with no chunk the run ends — it returns to its start pose over 3 s and releases torque, so the arm can fall. Support it, or pass --robot..
Your robot, your checkpoint#
Two cameras named like the model's — front and wrist — a six-value state, normalized units. LeRobot defaults to degrees: pass --robot.use_degrees=false. Dry run first; a returned chunk is not proof it is safe to execute.
agi rollout agi-wfm/so101-ttt --dry-run -- --robot.type=so101_follower --robot.use_degrees=false \
--robot.port=/dev/tty.usbmodemYOUR_ARM \
--robot.cameras= '{"front":{"type":"opencv","index_or_path":0,"width":640,"height":480,"fps":30},"wrist":{"type":"opencv","index_or_path":1,"width":640,"height":480,"fps":30}}'
Drop --dry-run when the ranges check out; keep the workspace clear.
Your own checkpoint. Any LeRobot checkpoint on the Hub — act, smolvla or diffusion, safetensors ≤ 3 GB — leases as hf:<org>/<repo> at the same price, with its own camera names, horizon and units: GET /v1/models/hf:<org>/<repo> returns them before any lease; /models#byo writes the command for your port and cameras.
Your own loop#
import agi # pip install agi-diy
with agi.Policy("agi-wfm/so101-ttt") as session:
chunk = session.predict({"observation.state": state, "observation.images.front": front, "observation.images.wrist": wrist}) # (32, 6) float32
From anything else: three REST calls and one WebSocket. Key in AGI_API_KEY (from /dashboard):
curl -s https://api.agi.diy/v1/sessions -H "authorization: Bearer $AGI_API_KEY" -H "content-type: application/json" -d '{"model":"agi-wfm/so101-ttt"}' # → {"id":"ses_…", …}
curl -s https://api.agi.diy/v1/sessions/$SESSION/wire -H "authorization: Bearer $AGI_API_KEY" # → {"url":"wss://…"} — open within 120 s
curl -s -X DELETE https://api.agi.diy/v1/sessions/$SESSION -H "authorization: Bearer $AGI_API_KEY" # the lease, and the bill, stop here
On the socket (agi-wire/1) the first text message is hello — read cameras, state_dim, action_horizon from it. Then, both ways:
frame = u32 little-endian header length · JSON header, space-padded to a multiple of 4 · blobs
→ {"type":"obs","request_id":"c7","cursor":0,"state":[6 floats],"images":[{"cam":"front","bytes":N},{"cam":"wrist","bytes":N}]} + the JPEG bytes, in that order
← {"type":"chunk","request_id":"c7","n":32,"dims":6,"dtype":"float32","tokens":32,"cursor":0} + n×dims float32, row-major
Coding agent (docs and models without a key; a key to lease):
claude mcp add --transport http agi https://api.agi.diy/mcp
Watch and keep#
Every session records what the policy saw and returned — frames, state, task, chunks, timing — unless created with record:false. Open it from your sessions; viewing is free, for 24 hours. Replay (POST /v1/sessions/:id/replay) is a new, billed session. Publish an ended session (toggle on its page): anyone can watch it at /s/<id>, no key, past the 24 hours. Keep it as training data: POST /v1/sessions/:id/export with {"label":"success"} writes one LeRobot v3.0 episode into a Hub dataset; agi rollout … --record <org>/<repo> does the same on a real arm.
Cost and limits#
One action = one token = $0.0001. The flagship returns 32 actions per call: $0.0032. New accounts receive 200,000 tokens ($20) once; there is no self-serve top-up yet — at zero, running sessions end with end_reason: credits and new leases answer 402 insufficient_credits. Spending: dashboard, agi usage, GET /v1/usage.
Limits#
A session lives 60–3600 s (ttl_s, default 600) and must connect within 60 s. REST bodies ≤ 16 KB; frames go on the wire — one message ≤ 8 MB, task text 512 characters. hf: checkpoints: 2 live sessions per account, 10 cold starts per hour, 300 s to warm. Traces: 24 h. A runner silent for 60 s ends its sessions runner_lost.
Errors#
Errors are {"error": — plus retry_after_s when waiting helps, doc_url to its row here, an x-request-id header to quote.
| If you see | Do |
|---|---|
invalid_key unauthorized | Sign in again or create a key; a shown-once key cannot be recovered. |
insufficient_credits | The grant is spent; no self-serve top-up yet. |
no_slots relay_down runner_offline no_wire internal export_failed export_timeout | Not your side: wait retry_after_s, check status. |
unknown_model not_found session_ended no_trace no_frame no_observations | Wrong id, another account's, or over — see GET /v1/models, end_reason. |
bad_request payload_too_large cross_site incompatible_features session_live export_running | message and param name the field; nothing is clamped or guessed. |
unsupported_policy unsupported_weights missing_config too_large hf_forbidden hub_unavailable rate_limited | Your hf: repo against the rules above; message names the file or cap. Hub down — retry. |
Links#
OpenAPI · Python SDK · JavaScript SDK · models · status · changelog