Docs
Overview
One versioned catalog powers everything: the website, live previews, source packs, both provider hand-offs, the REST API and the MCP server. Each item lives in catalog/packages/<id>/<version>/ with an asset.json, reusable source under src/, a demo.js and an index.html harness. Versions are immutable: their file digests are locked in catalog/catalog.lock.json.
Public endpoints need no account, cookie or key. Anything not in the published catalog (drafts, private customer data, admin functions) is not reachable through them.
REST & Markdown endpoints
| GET | /api/v1/visuals?q=&kind=&collection=&loopable=&target=&motion=&maxDuration=&sort=&limit=&cursor= | Search or list visuals (deterministic keyword + synonym search). |
| GET | /api/v1/visuals/{slug} | Latest published version with full manifest and version list. |
| GET | /api/v1/visuals/{slug}/versions/{version} | Exact, immutable manifest (params, composition, files with sha256, license, verification). |
| GET | /api/v1/visuals/{slug}/versions/{version}/files/{path} | One raw source file (exact version required). |
| POST | /api/v1/handoffs | Create an unlisted, immutable source pack. Returns id, URLs and a delete token. |
| GET | /api/v1/handoffs/{id} | Pack manifest (JSON). |
| DELETE | /api/v1/handoffs/{id} | Revoke a pack early (Authorization: Bearer <deleteToken>). |
| GET | /api/v1/handoffs/{id}/agent.md | Agent guide for the pack (Markdown). |
| GET | /api/v1/handoffs/{id}/files/{id}/{version}/{path} | One file of the pack. |
| GET | /api/v1/handoffs/{id}/download | The whole pack as a zip. |
| POST | /api/v1/bundles | Same request as handoffs, returns a zip directly; nothing is stored. |
| GET | /registry/{slug}/{version}/agent.md | Agent guide for one exact version (Markdown). |
| GET | /registry/{slug}/{version}/{path} | Raw files in the catalog layout (demos run from here). |
| GET | /api/oembed?url={visual page URL}&maxwidth=&maxheight= | oEmbed (type video): the embed snippet, its size and a thumbnail for a visual page. |
| GET | /embed/{slug} | The bare player other sites may frame. |
| GET | /random | Redirects to a random visual. |
| GET | /feed.xml | RSS of the fifty newest visuals, with their preview clips. |
| GET | /openapi.json | OpenAPI 3.1 description of the endpoints above. |
| POST | /api/mcp | MCP server (Streamable HTTP, stateless). |
Errors are JSON: { "error": { "code", "message", "details"? } } with a matching status (400 invalid, 404 not found, 410 revoked/expired, 413 too large, 415 not JSON, 429 rate limited). Version-pinned content is served with immutable caching and an ETag of its sha256. CORS is open for these read-only, credential-free endpoints. See also /llms.txt.
MCP server
Endpoint https://aipixelwizard.com/api/mcp, built with the official MCP TypeScript SDK (Streamable HTTP transport, stateless, JSON responses). Requests from browsers must come from an allowed Origin; command-line clients send none.
claude mcp add --transport http aipixelwizard https://aipixelwizard.com/api/mcp
codex mcp add aipixelwizard --url https://aipixelwizard.com/api/mcpsearch_visuals(query, filters?, limit?, cursor?)— short candidates with exact versions.get_visual(id, version?)— the version's agent guide plus structured manifest.get_source(id, version, paths?)— file text (bounded; large third-party files as URLs).prepare_handoff(items, composition?, disclosure?)— create a pack from catalog items. No free-form goals, no code execution, no model calls.get_handoff(packId)— a pack's agent guide and structured summary.
Source packs
A pack is an immutable snapshot: schema version, random id (130 bits), creation and expiry time (30 days), a content digest, every selected item (exact version, instance id, validated settings, role), the full dependency closure with per-file sha256, license notices, and a compatibility analysis (tested-together, compatible, needs-adaptation or conflict). The same pack is reused when switching between Claude and Codex.
Text the person typed into settings (titles, labels) is left out unless they choose to include it, and their description of the video is never sent to the server — it is added to the request in their browser. Packs are unlisted bearer links: anyone with the link can read them until they expire or are deleted with the delete token returned at creation.
curl -s https://aipixelwizard.com/api/v1/handoffs \
-H 'Content-Type: application/json' \
-d '{"items":[{"assetId":"chrome-material","params":{"tint":"#ffcf73"}},
{"assetId":"bouncing-balls"},{"assetId":"orbit-camera"}],
"composition":{"mode":"new","aspectRatio":"1:1","durationSeconds":8}}'Frame/time contract
Every demo exports createDemo({ canvas, width, height, params, fps, duration }) returning { renderFrame(info), dispose() }. info is { frame, fps, time, duration, durationFrames, progress }. Visual state is a pure function of the frame, params and seed — no wall clock, no unseeded randomness — so any frame can be rendered out of order. Physics is simulated once at a fixed timestep and baked per frame. Loops are built from whole cycles and verified by comparing frame 0 with the end state (progress 1).
Rendering video
apw-runtime/1.0.0/tools/render.mjs serves the pack on 127.0.0.1, loads the asset in headless Chrome/Chromium with ?capture=1, calls window.apw.renderFrame(n) for each frame, reads the canvas and pipes PNG frames to ffmpeg (H.264, yuv420p). 2D assets use a CPU-rasterised canvas in capture mode so frames are byte-identical between runs; 3D uses WebGL (SwiftShader when there is no GPU).
npm install
npm run render -- chrome-material/1.0.0 --param tint=#ffcf73 --out gold.mp4
npm run render -- bouncing-balls/1.0.0 --params my-settings.json --width 1920 --height 1080 --fps 60The sample video on each visual page is that same pipeline at default settings, rendered when the version was verified. It is not an export of customised settings.
Embed, watch & share
Every visual can sit on another web page. The Embed button on its page copies a snippet like this one:
<iframe src="https://aipixelwizard.com/embed/moon-pearl" width="480" height="600"
style="border:0;border-radius:8px;max-width:100%" allow="autoplay; fullscreen"
loading="lazy" title="The Moon Is a Pearl, AI Pixel Wizard"></iframe>/embed/{slug} is a bare player (and /embed/topics/{id} or /embed/collections/{id} plays a whole shelf, each clip in turn): the clip loops, muted, on black, with a small credit that links back. It is the only path other sites may frame (every other page sends frame-ancestors 'self'), it loads no analytics, and it waits for a click when the viewer has asked for reduced motion. Visual pages also advertise an oEmbed endpoint, so tools that support oEmbed turn a pasted link into the player: GET /api/oembed?url=https://aipixelwizard.com/visuals/moon-pearl answers with type: "video", the snippet, its width and height and a thumbnail.
Export your version on a visual page makes an MP4 of your settings in your own browser: a sandboxed frame (/registry/{slug}/{version}/export.html, under the demo policy) renders every frame with the visual's code, encodes H.264 with WebCodecs and hands the file back. Nothing is uploaded, typed text included. Browsers without an H.264 encoder are told so and pointed at the sample video or the render command. GIF does the same as a looping GIF, 400 px wide at 15 frames a second, for READMEs and chats that will not play a video.
Links unfurl into a 1200×630 card (the clip on the programme monitor, its title beside it) for visuals, collections and topics, and visual links carry the 480 px clip as og:video, which chat apps play inline. A visual link with ?s= (base64url JSON of the changed settings; the older ?settings={json} still works) opens that version of it; typed text is never put in a link. A whole sequence travels the same way: Share on Your sequence makes a /selection?recipe=... link (clips, exact versions and settings), and whoever opens it can add those clips to their own sequence in one go. /watch, /collections/{id}/watch and /topics/{id}/watch play every clip full screen, one after another; ?v={slug} starts at a clip, and the address follows what is on screen.
More addresses worth knowing: /watch?ids=a,b,c plays exactly those clips in that order (up to 24) and unfurls into its own card; /watch/shuffle plays everything in a random order kept in the link (?shuffle={seed}); /wall shows every clip at once (?track=effects, ?portrait=1); /today always opens the clip of the day; /topics lists every topic and collection (one of them, /topics/phones, holds the clips taller than wide; /phones, /spooky and /halloween are short ways in); /random takes ?topic= or ?collection= to pick within one shelf; /explore?q=… searches unfurl into a card of their top results; and /make/{slug} opens the title maker on one look. Every page that unfurls also carries its clip as og:video.
Exports run in the visitor's browser (WebCodecs): any visual with settings as MP4 or GIF; any visual as a vertical 1080×1920 MP4 for TikTok, Reels and Shorts (the clip over a blurred copy of itself); a full-size still PNG of the frame on screen (Save this frame, on visual pages, in the title maker and in the full-screen player); a whole sequence as one MP4, each clip in turn; and a one-click vertical montage of the first four clips of any topic, collection, Explore search or the latest trends. Nothing is uploaded.
Adding an asset
npm run catalog:scaffold -- my-effectcreatescatalog/packages/my-effect/1.0.0/with a draftasset.json, stubs and the standardindex.html.- Write the reusable source in
src/(no wall clock, no unseeded randomness) and a smalldemo.js. Import other packages only by exact-version relative paths and declare them independencies(scopesourceordemo). The only bare import allowed isthree. - Describe parameters (with plain-language labels), presets, output, composition notes, at least two reuse ideas, limitations, license and provenance.
npm run catalog:build -- --devvalidates the schema, files, import graph and parameters.npm run catalog:verify -- my-effectrenders the real default sample, poster and preview clip, and checks determinism and any loop claim.- Review rights (code, shaders, fonts, textures, media) and record the owner's approval in
catalog/rights.json. Unapproved items stay out of public listings. - Set
statustopublishedand runnpm run catalog:build -- --lock. From now on the version is immutable; changes need a new version folder.
Security & privacy model
- Only files listed in a version's inventory can be read; paths are matched exactly, never joined from user input. Packages may not contain symlinks or dotfiles.
- No user-provided code, expressions, shader strings, package scripts or file paths are executed or accepted. Parameters are validated against each asset's schema.
- Live previews run in
<iframe sandbox="allow-scripts">(opaque origin, no cookies or storage of this site) with a strict Content-Security-Policy. Messages in both directions are shape-checked. - Rate limits apply per IP. The app does not log request bodies. See Privacy.
Client examples
// Discover → manifest → source, with failure handling (Node 18+ / browser)
const base = 'https://aipixelwizard.com';
const res = await fetch(`${base}/api/v1/visuals?q=${encodeURIComponent('chrome balls')}&limit=3`);
if (!res.ok) throw new Error((await res.json()).error.message);
const { results } = await res.json();
const pick = results[0];
const manifest = await (await fetch(`${base}/api/v1/visuals/${pick.id}/versions/${pick.version}`)).json();
for (const file of manifest.files.filter((f) => manifest.implementation.reusable.includes(f.path))) {
const r = await fetch(file.url);
if (r.status === 410) throw new Error('This version was revoked; pick another.');
const text = await r.text();
// verify file.sha256 before writing it into the project
}