DocumentationQuickstart
Documentation

Quickstart

The plain three.js starter project installs stylized-town@0.4.0, renders a small snapped scene with one part swap and one recolour, and checks it in Chromium. The same steps are tested on a fresh copy of this starter with Node.js, pnpm and Chromium only.

1. Download the starter

Terminal
curl -fsSLO <site origin>/starters/starter-vanilla.tgz
tar -xzf starter-vanilla.tgz
cd starter-vanilla

starter-vanilla.tgz (15 KB, sha256 17a7f8896161a058…) contains .gitignore, README.md, index.html, package.json, pnpm-lock.yaml, pnpm-workspace.yaml, src/main.ts, src/scenes/starter.scene.json, tsconfig.json. For React use starter-r3f.tgz and the React Three Fiber guide.

What the placeholders in the commands mean
  • <registry origin> and <site origin>: the address of this website, scheme and host only (for example https:// followed by the host name in your address bar, no path). The website, the account service and the catalog registry share this one origin; the MCP server is at <registry origin>/mcp. The CLI accepts https only (plain http only for a local loopback registry).
  • <API token from your account page>: create one on your account page after signing in. It installs only what your account is entitled to; the free sampler needs no token.
  • <folder with the SDK tarballs>: the folder with the SDK .tgz files. The SDK packages come as tarballs, not through the public npm registry. See what you need.
  • Run npx agent-ready-assets … only inside the project where the SDK is installed: npx then runs the project's own CLI from node_modules/.bin. Outside such a project npx would look for a package of that name on the public npm registry, where we publish none — do not run it there (pnpm exec agent-ready-assets … never downloads anything).

For the next steps you need

  • Node.js 22 and pnpm 11.
  • The SDK tarballs (what you need).
  • For step 3, an API token whose account owns the kit. No kit access? Follow the free sampler: the same steps, no account and no token.

2. Add the SDK tarballs and install

The SDK 0.5.1 is not on the public npm registry. The starter resolves 4 tarballs from vendor/ (agent-ready-assets-asset-cli-0.5.1.tgz, agent-ready-assets-contracts-0.5.1.tgz, agent-ready-assets-customization-0.5.1.tgz, agent-ready-assets-runtime-three-0.5.1.tgz): package.json and pnpm-workspace.yaml point the SDK packages at them and the lockfile pins their bytes. Copying all 5 tarballs you received does no harm.

Terminal
mkdir -p vendor
cp <folder with the SDK tarballs>/*.tgz vendor/
pnpm install --frozen-lockfile

Check the setup. doctor uses no network and no Blender, and prints where the SDK's TypeScript declarations are: they ship next to the JavaScript (node_modules/@agent-ready-assets/runtime-three/dist/index.d.ts), so editors, tsc and coding agents find them from a plain import.

Terminalcopy as is
npx agent-ready-assets doctor --no-blender

3. Install the kit

Tell the CLI where the registry is (the address of this website) and who you are. The CLI reads the token from this environment variable, sends it only to the registry and never writes it to a file; do not commit it. (An MCP configuration such as .mcp.json does hold the token in a file: keep that file out of version control, see MCP.) When the account does not own the kit this step stops with ENTITLEMENT_REQUIRED (exit 3) and writes nothing: use the free sampler instead.

Terminal
export AGENT_READY_ASSETS_REGISTRY=<registry origin>
export AGENT_READY_ASSETS_TOKEN=<API token from your account page>
Terminalcopy as is
pnpm run kit:add
pnpm run validate

kit:add runs agent-ready-assets kit add $(node -p "require('./src/scenes/starter.scene.json').kit") --to public/assets: every file's sha256 is checked before anything is written, and assets.lock.json pins the install: commit it (the starter's .gitignore lists it because the starter is a template: delete that line in your project). validate runs agent-ready-assets verify && agent-ready-assets scene validate src/scenes/starter.scene.json --resolve: the installed files against the lockfile and that one scene file against the installed manifests, printing every snapped transform. Check any other scene file the same way: pnpm exec agent-ready-assets scene validate <file> --resolve.

4. Build and verify in a browser

Terminalcopy as is
pnpm run typecheck
pnpm run build
pnpm exec playwright install chromium
pnpm run verify-project

verify-project renders the project's configs and scenes with the installed SDK and writes LOCALLY_VALIDATED evidence and screenshots to .agent-ready-assets/verify/.

Optional, off by default: npx agent-ready-assets verify-project --browser chromium --share-result also sends one anonymous receipt of a passed run to your registry (cli_install_verified with the label, the kit's major version and the SDK's minor version — no token, cookie, paths, names or hashes). Nothing is sent without the flag, for a failed run or with DO_NOT_TRACK=1; the label and exit code never depend on it.

5. Run it

Terminal
pnpm dev

Open the printed URL: orbit, pan and zoom with the mouse. After step 3 everything works offline. pnpm run build copies public/assets — every installed kit file — into dist/. The licence says what a published build may contain.

What the starter renders

src/main.ts loads the installed kit, assembles src/scenes/starter.scene.json with the SDK and passes { optimize: true } (SDK 0.5.1): repeated meshes are drawn with GPU instancing, which cuts draw calls and leaves the image, customization, clips, picking and release() unchanged. Keep it when your scene grows (measurements; when not to batch an instance).

src/main.ts
src/main.ts
// Stylized town starter (plain three.js). Renders src/scenes/starter.scene.json — a small scene 1.1.0 whose props and
// house are snapped onto the road tiles with attach_to connectors, with one part swap and one recolour — from the kit
// the agent-ready-assets CLI installed into public/assets. No Blender, no network at runtime (the GLBs are served from
// this project). Edit the scene, then check it with `pnpm exec agent-ready-assets scene validate src/scenes/starter.scene.json`.
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { AssetLibrary, isCustomizationError } from '@agent-ready-assets/runtime-three';
import starter from './scenes/starter.scene.json';

// The scene pins its kit ("kit@x.y.z"); `agent-ready-assets kit add <kit@x.y.z> --to public/assets` installed it here.
const KIT_URL = `/assets/${starter.kit.split('@')[0]}/kit.json`;
const status = document.getElementById('status')!;

const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.outputColorSpace = THREE.SRGBColorSpace;
document.body.appendChild(renderer.domElement);

const scene = new THREE.Scene();
scene.background = new THREE.Color('#e9e4da');
scene.add(new THREE.HemisphereLight(0xffffff, 0x8d7c64, 1.4));
const sun = new THREE.DirectionalLight(0xffffff, 2);
sun.position.set(6, 10, 7);
scene.add(sun);
const ground = new THREE.Mesh(new THREE.PlaneGeometry(200, 200), new THREE.MeshStandardMaterial({ color: '#8fa86a' }));
ground.rotation.x = -Math.PI / 2;
ground.position.y = -0.02;
scene.add(ground);

const camera = new THREE.PerspectiveCamera(40, window.innerWidth / window.innerHeight, 0.05, 500);
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;

/** 3/4 view of everything that was assembled; orbit, pan and zoom with the mouse. */
function frame(target: THREE.Object3D): void {
  const box = new THREE.Box3().setFromObject(target);
  const center = box.getCenter(new THREE.Vector3());
  const radius = Math.max(box.getSize(new THREE.Vector3()).length() / 2, 1);
  const dist = radius / Math.sin(THREE.MathUtils.degToRad(camera.fov / 2));
  camera.position.copy(center).add(new THREE.Vector3(0.6, 0.55, -1).normalize().multiplyScalar(dist * 0.9)); // from the street side: the buildings face the road
  controls.target.copy(center);
  controls.update();
}

window.addEventListener('resize', () => {
  camera.aspect = window.innerWidth / window.innerHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(window.innerWidth, window.innerHeight);
});

async function main(): Promise<void> {
  const library = await AssetLibrary.fromKit(KIT_URL); // every manifest and GLB is checked against its sha256 pin first
  // Validated and snapped in one step (schema, pinned versions, declared capabilities, connector types, mates within
  // 1 mm, no overlaps); all-or-nothing: an invalid scene adds nothing and throws a structured CustomizationError.
  // `optimize: true` (SDK 0.5.0) draws repeated meshes with GPU instancing: fewer draw calls, and the image,
  // customization, clips, picking and release() stay exactly as without it. Keep it on as your town grows. Instances
  // you animate, re-material or decorate per object yourself: leave them out by id, { optimize: { exclude: ['house-1'] } }
  // (README, "When not to batch an instance").
  const town = await library.assembleScene(starter, { optimize: true });
  scene.add(town.group);
  frame(town.group);
  const snapped = town.resolved.filter((i) => i.attach_to).length;
  const draws = town.optimization?.stats();
  status.textContent = `${starter.kit}: ${town.instances.length} instances, ${snapped} snapped with attach_to${draws ? `, ${draws.drawsAfter} draw calls (${draws.drawsBefore} without optimization)` : ''}`;
  let first = true;
  renderer.setAnimationLoop(() => {
    controls.update();
    renderer.render(scene, camera);
    if (first) renderer.domElement.dataset.ready = 'true'; // the town is on screen
    first = false;
  });
}

main().catch((error: unknown) => {
  const message = isCustomizationError(error) ? `${error.code}: ${error.message}` : String(error);
  status.textContent = message;
  status.dataset.state = 'error';
  renderer.domElement.dataset.error = message;
  console.error(message);
});

Next

The starter's README

Stylized town starter — plain three.js

A minimal Vite + TypeScript + three.js project that installs an Agent-Ready Assets kit and renders a small town scene: road tiles with a lamp, a bench and a house snapped onto them with connectors (attach_to), the house with one part swap and one recolour through the kit's declared capabilities. You can orbit, pan and zoom with the mouse.

Needs only Node.js 22, pnpm 11 and a browser — no Blender, no Python, no Blender MCP, and no network at runtime (the GLBs are served from this project's own public/ folder).

Set up

  1. Put the five pre-release SDK tarballs you received into vendor/ (agent-ready-assets-contracts-0.5.1.tgz, …-customization-…, …-runtime-three-…, …-runtime-r3f-…, …-asset-cli-0.5.1.tgz; the SDK is not on the public npm registry yet). pnpm-lock.yaml pins their bytes.
  2. pnpm install
  3. Tell the CLI where the registry is and who you are (never commit the token):
Terminal
   export AGENT_READY_ASSETS_REGISTRY=<registry origin>
   export AGENT_READY_ASSETS_TOKEN=<entitlement token>
  1. pnpm run kit:add — installs the kit pinned by src/scenes/starter.scene.json (its kit field) into public/assets/, verifying every file's sha256 before writing, and writes assets.lock.json (commit it).
  2. pnpm run validate — checks the installed files against the lockfile and the scene against the installed manifests (--resolve prints every snapped instance's world transform and the measured connector mate).
  3. pnpm dev and open the printed URL.
  4. pnpm run verify-project — renders the project's configs and scenes in Chromium and writes LOCALLY_VALIDATED evidence and screenshots to .agent-ready-assets/verify/ (needs Playwright's Chromium: pnpm exec playwright install chromium).

After step 4 everything works offline.

Change the town

Edit src/scenes/starter.scene.json (scene schema 1.1.0):

  • customize takes only the capabilities the asset declares. List them, with the parts every swap allows, the palette swatches and the hideable parts: pnpm exec agent-ready-assets inspect <kit/asset@x.y.z> --capabilities.
  • Snap a new instance with attach_to { instance_id, anchor_id, connector } instead of coordinates; the connector types must be compatible (pnpm exec agent-ready-assets inspect <kit/asset@x.y.z> --connectors lists them). A snapped instance has no position_m / rotation_euler_rad; one instance (here road-1) is placed explicitly.
  • Run pnpm run validate after every change. Anything the kit does not declare (a new roof shape, extra storeys, a free colour on a palette, an unknown part, an occupied or incompatible connector) is refused with a structured error (UNSUPPORTED_OPERATION, INVALID_PARAMETER, INCOMPATIBLE_PART, SNAP_*, …) — never approximated.

src/main.ts loads the kit index (AssetLibrary.fromKit), assembles the scene with library.assembleScene (validated, snapped, all-or-nothing) and renders it with three.js OrbitControls.

Large towns: keep optimize: true

library.assembleScene(starter, { optimize: true }) (SDK 0.5.0) draws every mesh that several instances share (road tiles, lamps, benches, the windows of a house, …) with one GPU-instanced draw, so draw calls stay low as the town grows (the towns real coding agents built in the M3.5B trials: 169–264 draw calls without it, 63–93 with it). It changes nothing else: the image, customized instances, clips (playClip), raycasts (town.optimization.pick(raycaster) returns the instance and part under the pointer) and release() work as before, and an instance you move or hide yourself is drawn by its own objects from that frame on. town.optimization.stats() shows the draw calls with and without it; town.customize(id, values) re-customizes one instance in place.

When not to batch an instance

The optimization handles these cases by itself: playClip and town.customize hand an instance back and batch it again, and an instance you move, hide or give another material is drawn by its own objects from that frame on. Leave an instance out of the batches when your code changes it often or decorates it per object:

  • you animate it yourself (a mixer, a tween — not playClip): every time it rests, all batches are rebuilt;
  • you swap its materials at run time (hover or selection highlight) or drag it with a gizmo;
  • it carries a per-object render effect — onBeforeRender, a customDepthMaterial, an outline or selection pass, a picking render that draws meshes one by one: while batched, its meshes are invisible to the renderer, so these effects never see them;
  • a configurator re-customizes it many times in a row (each town.customize rebuilds all batches).
ts
const town = await library.assembleScene(starter, { optimize: { exclude: ['house-1'] } }); // scene instance ids
// or { optimize: 'strict' }: only unmodified, clip-free instances are batched; customized ones keep their own meshes

Exclude by instance id: the exclusion survives town.customize('house-1', …). Raycasting needs no exclusion (town.optimization.pick(raycaster) returns the instance and part under the pointer). Details: the website's Performance page (/docs/performance, "When not to batch an instance").

TypeScript

The SDK packages ship their declarations next to their JavaScript (node_modules/@agent-ready-assets/runtime-three/dist/index.d.ts; pnpm exec agent-ready-assets doctor prints the path). Import from the package name; no deep paths.