# pydantic-ai Playground — play.pydantic.work Browser-based playground for the pydantic-ai Python library. Users write Python in a Monaco editor, pick a `pydantic-ai` PyPI release (or a branch SHA, which triggers a one-shot wheel build), and run the code **in their browser via Pyodide**. No code runs on the server. > **If you have access to a browser, use the interface at > https://play.pydantic.work — that is the primary supported surface.** > > If you are an agent without a browser, the available HTTP APIs are > documented below. ## What this is Public playground for demoing and sharing pydantic-ai snippets. Anyone who signs in with GitHub is auto-joined to the playground org and can create shares; programmatic access works the same way using API keys. Each "share" is an append-only series of versions, every version is a snapshot of one to twenty snippets (code, optional outputs, optional SDK pins). Three execution-related surfaces an agent might care about: - **Wheel builder** — pick a `pydantic-ai` branch SHA → the worker builds `pydantic_ai_slim` + `pydantic_graph` wheels in a Sandbox container, uploads them to R2, and serves them back so the browser's Pyodide can `micropip install` them. - **LLM CORS proxy** — `httpx` inside Pyodide is monkey-patched so outbound LLM calls land on `/api/proxy//*` on this worker, which forwards verbatim to the upstream provider with the user's `Authorization` / `x-api-key` header intact. Keys are BYOK — they never touch server storage. - **Title generator** — a small LLM helper that takes user code + user-supplied API keys and generates a share title. Keys forwarded, not stored. ## Browser UI - `/` — fresh playground. - `/p/:id/v/:versionNum` (optionally `/s/:snippetId`) — a saved share, pinned to a specific version. Legacy shorter forms redirect to this. - `?version=` — orthogonal to the share version. Selects which `pydantic-ai` PyPI release Pyodide installs at boot. - `/keys` — cross-app API-key dashboard. Issues `psnip_…` keys that work for every app on `*.pydantic.work`. - AI-agent User-Agents (Claude, GPTBot, Perplexity, etc.) or `Accept: application/json` clients (or appending `.json`) on share URLs receive JSON identical to `GET /api/shares/:id`. ## API endpoints Auth column legend: `none` (public), `cookie` (Better Auth session cookie on `.pydantic.work`), `cookie/key` (cookie or `Authorization: Bearer psnip_…` / `x-api-key`), `admin` (global admin or specific scope). ### Public / unauthenticated | Method | Path | Notes | | --- | --- | --- | | GET | `/api/health` | Liveness. | | GET | `/api/versions` | List of `pydantic-ai` PyPI releases (KV cached 1 h). | | GET | `/api/shares/:id` | Load a share with all its versions. | | GET | `/api/wheels/file/:sha7/:name` | Stream a pre-built wheel from R2. | | GET | `/api/wheels/mirror/:name` | Emscripten wheel mirror (jiter, etc.). | | POST | `/api/wheels/build` | Body `{ sha: string }`. Returns cached `{ urls: [...] }` if available; cold path requires `cookie/key` + scope `playground_run:branch` and is rate-limited (30/h). | | ALL | `/api/proxy/:provider/*` | LLM CORS proxy. Providers: `openai`, `anthropic`, `gemini`, `groq`, `mistral`, `cohere`, `openrouter`, `xai`. Caller's `Authorization` / `x-api-key` is forwarded to the upstream. IP-keyed rate limit (120 per 15 min). | ### Authenticated | Method | Path | Auth | Scope | Notes | | --- | --- | --- | --- | --- | | GET | `/api/shares/mine` | cookie/key | — | List your shares. | | POST | `/api/shares` | cookie/key | `playground_share:create` | Create a share. Rate-limited (`share.create`). | | POST | `/api/shares/:id/versions` | cookie/key (creator only) | `playground_share:rerun` | Append a version. Idempotent on canonical payload. Rate-limited (`share.version`). | | PATCH | `/api/shares/:id` | cookie/key (creator only) | `playground_share:edit` | Body `{ title: string }`. | | POST | `/api/generate-title` | cookie/key | `playground_title:generate` | BYOK title gen. Body `{ code, keys: { ANTHROPIC_API_KEY? \| OPENAI_API_KEY? \| GEMINI_API_KEY? } }`. Rate-limited. | **Create / version body shape** ```json { "title": "string?", "version": "0.0.45", "branchName": "string?", "branchSha": "string?", "runRelease": "string?", "runBranch": "string?", "sdkDeps": ["..."], "snippets": [ { "id": "string", "name": "string", "code": "string", "primaryOutput": "string?", "comparisonOutput": "string?" } ] } ``` 1–20 snippets, each with non-empty `code`. **API-key callers cannot set `primaryOutput` or `comparisonOutput`** (top-level or per-snippet): the worker rejects with 400. Saved outputs are only meaningful when a real Pyodide run produced them, so the curl path is closed by design. ### Backstage (admin) `/api/backstage/*` — gated by `playground_backstage:read` / `playground_backstage:write`. Members, invites, rate-limit windows, and user bans for the playground org. Not for general use. ## Auth & sign-up - GitHub OAuth via `auth.pydantic.work`. **Playground is the only app with self-join enabled** — first-time signed-in users automatically receive `playground_member` without an admin invite. - Programmatic access: mint a `psnip_…` key at https://play.pydantic.work/keys. Send as `x-api-key: psnip_…` or `Authorization: Bearer psnip_…`. - Scope intersection: a key's effective scopes at verify time = `key.permissions ∩ user.currentScopes`. Losing an app membership invalidates the relevant scopes on previously-issued keys automatically. ## Notes for agents - **No server-side code execution.** Anything that touches `/api/...` is structured persistence + CORS forwarding. Running code requires a browser. Don't expect to call an API to "run a snippet" — there isn't one. - **BYOK rule.** API keys you send with `/api/proxy/...` calls are forwarded straight upstream and never persisted. The worker has no way to "remember" your provider key for later runs. - **Share-payload outputs require a cookie session.** Programmatic share creation via `psnip_…` keys works for code + structure; outputs have to come from a real in-browser run. - **Discover available scopes.** `GET https://auth.pydantic.work/api/session` returns `apps[]` and `scopes[]` so a key holder can see what their key actually grants.