# NIUA > Catch broken robot files before they break your simulation. NIUA is the gate for agent-built work, robots for simulation first. Your agent builds a file (URDF, GLB, WAV) in a real tool; NIUA checks it against a versioned contract, returns typed failures with a fix for each, and signs an Ed25519 receipt on pass. The gate runs on CPU and checks are free. Hosted media generation is a separate, legacy paid add-on (see Generation). The gate answers one question for an agent: is this file done? A check runs your artifact against a contract (`niua.contract/v1`). On `pass`, NIUA signs a receipt (`niua.receipt/v1`) that anyone can verify offline against NIUA's public key. On `fail`, you get a list of failures, each with the rule, the path, what was expected, what was found, and how to fix it. Repair the file, check again. Base URL: `https://api.ohao.tech`. OpenAPI 3.1: https://api.ohao.tech/openapi.yaml. Authenticate with `Authorization: Bearer niua_live_...`. ## Quick start 1. Connect. MCP endpoint: `https://mcp.ohao.tech/mcp` (streamable HTTP, OAuth on first use). Claude Code: `claude mcp add --transport http niua https://mcp.ohao.tech/mcp`. Other clients: https://ohao.tech/niua/connect. 2. Pick a contract. Built-ins: `articulated.sim-ready@1` (a URDF or MJCF robot or articulated object, run in MuJoCo by simcheck), `urdf.sim-ready@1` (static robot checks), `motion.g1-safe@1` (Unitree G1 motion clip, MuJoCo replay on CPU), `splat.sim-ready@1` (Gaussian splat scene plus its collision mesh), `glb.prop@1` (engine-ready prop), `glb.prop-strict@1` (mobile budget), `wav.sfx@1` (sound effect), `wav.voice@1` (voice line). Every rule: https://ohao.tech/niua/contracts. MCP: `list_contracts`, `get_contract {id}`. 3. Build the file in a real tool (a robot description for a simulator, Blender, a DAW, an engine), export it, upload it (`create_upload`, then `finalize_upload`), and call `check_artifact {key, contract}`. 4. On `verdict: "fail"`, read `failures[]`: each has `rule`, `path`, `expected`, `got`, and `fix`. Repair the file in your tool, upload again, check again. Match on `rule`, never on prose. 5. On `verdict: "pass"`, keep the `receipt`: an Ed25519-signed `niua.receipt/v1` over the file hash, contract, and checker version. Anyone can verify it: `GET https://api.ohao.tech/api/v1/receipts/{receipt_id}` (no auth) or offline against `https://api.ohao.tech/.well-known/niua-receipt-keys.json`. MCP: `verify_receipt {receipt_id}`, or `verify_receipt {receipt}` with the whole object for a sample receipt (signed, not stored). No account yet? `GET https://api.ohao.tech/api/v1/playground/samples` lists every real sample file you can check without a key (`niua.playground-samples/v1`): what each file is in one sentence, the contracts it is published for, the exact rules the raw file fails, what the fix changed, and the copy-paste `curl` for the raw and fixed check (plus `simulate` or `repair` where they apply). Over MCP: `list_samples`, then `check_sample {id, contract, variant}`. Human-readable version: https://ohao.tech/niua/how-it-works ## Generation What is true today: - The gate is CPU only and free to check. It does not need a GPU, Modal or an LLM. - Hosted media generation (`/api/v1/generate/*`, the Media tools below) is a separate, legacy paid add-on from when NIUA was an asset generation API. It is on today: `GET https://api.ohao.tech/health` reports `generation: "hosted"`. A gateway started with `NIUA_GENERATION=off` serves the gate only and answers those routes with 404 `not_found`. - New embodied work (robots, motion, simulation) generates on your own GPU. Ohao spends nothing on GPUs for it. ## Your GPU, our gate (self-host) `niua-runner` (a Node CLI and a Docker image on `node:22` with Python, MuJoCo 3.16.0 and pyarrow for the dataset recipes and the LeRobot export, all on CPU; proprietary, given to customers on request) checks a file and verifies its receipt offline: - `niua-runner check clip.csv --contract motion.g1-safe@1` uploads, checks, verifies the receipt locally and writes `receipt.json` (needs `NIUA_API_KEY`; `NIUA_API_URL` points it at your own NIUA). - `niua-runner keys --out keys.json` and `niua-runner contracts --out contracts.json`, then `niua-runner verify receipt.json --file clip.csv --keys keys.json --contracts contracts.json` verifies offline, including that the receipt met a contract NIUA publishes (`--require-builtin` exits 1 if not). `niua-runner contracts motion.g1-safe@1` prints the exact rules. - `niua-runner repair clip.csv` repairs a G1 clip that falls over (`POST /api/v1/repair`), verifies the signed repair document locally, checks the repaired clip against `motion.g1-safe@1` and verifies the receipt. MCP: `repair_motion {key}`, then `check_artifact` on the key it returns. - Every niua-runner command takes `--json`: stdout is one object with a plain-English `summary`, `ok`, `exit` and the exact fields (`verdict`, `contract`, `failures[]` with `fix`, `receipt_id`). Errors are `{ "ok": false, "error": { "code", "message", "next" } }`. - `niua-runner simulate receipt.json` sweeps a passed URDF robot's joints in MuJoCo (simulate_artifact), verifies the signed report and its replay locally, and writes `simulation.json`; `verify` checks that report offline too. - `niua-runner dataset generate`, `dataset verify` and `dataset export --format lerobot` run on your machine, offline: generate runs a CPU recipe, verify checks every episode (and can re-simulate a sample), export verifies the source and writes LeRobot v3.0 (runner README). - Exit codes: 0 pass, valid or collision free; 1 fail, invalid or collides; 2 usage or network error. No runner needed to verify: any receipt checks against `https://api.ohao.tech/.well-known/niua-receipt-keys.json` with Ed25519 over the canonical JSON (see Receipts). Self-hosting the gate itself is offered to customers by arrangement. NIUA's own code is not published. Simulation only, no hardware tested. Page: https://ohao.tech/niua/self-host ## Tracks One gate, many contracts. Status labels are exact: live means a built-in contract ships and real receipts exist; next means planned and not built. - Robotics, sim-ready (new, live; the lead track): `articulated.sim-ready@1` and `urdf.sim-ready@1`. `articulated.sim-ready@1` runs a URDF or MJCF (zip with meshes, or bare) in MuJoCo on CPU with simcheck (Apache-2.0): contacts at rest, joint sweeps, drops, mass and inertia against the geometry, meshes MuJoCo collides as convex hulls; every simcheck error fails with its check id, and the receipt names the simcheck and MuJoCo versions. `urdf.sim-ready@1` is the earlier static check; both remain valid. Simulation only, no hardware tested: NIUA has no robot hardware. Static checks at the zero joint pose; they do not prove sim-to-real fidelity. Try it: https://ohao.tech/niua/playground?starter=robot-arm - Motion, humanoid (new, live): `motion.g1-safe@1`. Simulation only, no hardware tested: a Unitree G1 clip is measured and replayed in MuJoCo on CPU. Rules: https://ohao.tech/niua/contracts#motion-rules. The clip is `niua.g1-motion/v1` CSV (a `# niua.g1-motion/v1 fps=50` line, then `root_x,root_y,root_z,root_qw,root_qx,root_qy,root_qz` and the 29 G1 joint names as columns, radians, at most 60 s). REST: upload it as `text/csv`; MCP: `create_upload` with `content_type: text/csv`, then `check_artifact {key, contract: "motion.g1-safe@1"}`; CLI: `niua-runner check clip.csv --contract motion.g1-safe@1`. - Splat scenes (new, live): `splat.sim-ready@1`. A Gaussian splat and its collision mesh, checked on CPU: they share one frame (on a fail the gate prints the transform that makes them agree, exact for axis-aligned rotations and unit conversions, or says the collider cannot pin the frame), the floor is level, the collider is under the splat's floor-plane footprint, few floaters, and boxes dropped in MuJoCo rest on the surface. It does not judge how the splat looks, relighting or contact accuracy; metric scale is only plausible unless you declare a reference measurement, and then only consistent with it. Rules: https://ohao.tech/niua/contracts#splat-rules. REST, MCP and CLI as below. Try it with no account: `POST https://api.ohao.tech/api/v1/playground/samples/splat-room/check?contract=splat.sim-ready@1&variant=raw` fails only `frame.matches_collider` and names the rotation that fixes it; `variant=fixed` passes with a signed receipt (files: https://ohao.tech/landing/playground/splat-room/raw.zip, https://ohao.tech/landing/playground/splat-room/fixed.zip). A starter card on the playground page is next. - Games, the first proof (live): `glb.prop@1`, `glb.prop-strict@1`, `wav.sfx@1`, `wav.voice@1`. - World models (next, planned): turning generated scenes into sim-ready assets, and signed provenance for training data. No contract exists yet. ## Robotics claims and their evidence Every robotics claim links a live demo, the real input file and a receipt you can verify. Measured numbers carry their method and date. All of it is simulation only, no hardware tested. Machine-readable: https://ohao.tech/claims.json - `urdf-gate`: NIUA reads a robot description (URDF) against `urdf.sim-ready@1`: one kinematic tree, real mass and inertia, a collider on every visible link, sane joint limits, meshes that resolve, metres. Demo: https://ohao.tech/niua/playground?starter=robot-arm. Input: https://ohao.tech/landing/playground/robot-arm/raw.zip. Receipt: https://ohao.tech/niua/receipts/19b7fb87-63dd-4d3a-8ad7-51a275a415ea (covers https://ohao.tech/landing/playground/robot-arm/fixed.zip). - `urdf-oracle`: 8 broken copies of our desk arm, one break each. Flagged: NIUA gate 8/8, yourdfpy 1/8, MuJoCo 2/8 (compiled the other 6); measured 2026-10-15. Method: 13 fixtures (5 good, 8 broken); yourdfpy 0.0.60 `URDF.load` with meshes then `validate()`; MuJoCo 3.16.0 `MjModel.from_xml_path`. Result: https://ohao.tech/evidence/urdf-oracle/result.json. Demo: https://ohao.tech/niua/playground?starter=robot-arm. Input: https://ohao.tech/evidence/urdf-oracle/fixtures.zip. Receipt: https://ohao.tech/niua/receipts/19b7fb87-63dd-4d3a-8ad7-51a275a415ea (the good arm every broken copy starts from). - `motion-g1`: a real NVIDIA Unitree G1 clip (the first 3.5 s of GR00T-WholeBodyControl `macarena_001__A545`) passes `motion.g1-safe@1` in MuJoCo 3.16.0 on CPU on the production gate, after four seeded mistakes were fixed from typed failures; measured 2026-10-15 with timestep 0.002 s and joint PD kp 500 kv 10.32. Record: https://ohao.tech/landing/playground/g1/production.json. Demo: https://ohao.tech/niua/playground?starter=humanoid-g1. Input: https://ohao.tech/landing/playground/g1/raw.csv. Receipt: https://ohao.tech/niua/receipts/55b8224b-7908-4c74-8f81-4738cd1c906f (covers https://ohao.tech/landing/playground/g1/fixed.csv). - `auto-sim`: Claude Code repaired the arm from the gate's typed failures until it passed (every guessed value labelled an assumption); MuJoCo 3.16.0 refused the original and loads the repaired file, which stays finite for 2 s under gravity from a bent pose; measured 2026-10-15. Record: https://ohao.tech/auto-sim/run.json. Demo: https://ohao.tech/niua/playground?starter=robot-arm. Input: https://ohao.tech/auto-sim/input.zip. Receipt: https://ohao.tech/niua/receipts/48e24d5c-3bb1-476d-8d1f-b5cd52bb7545 (covers https://ohao.tech/auto-sim/output.zip). ## What NIUA does not do - Simulation only, no hardware tested. NIUA has no robot hardware: every robot check and run happens in software, the gate on our servers and the simulation in MuJoCo. Nothing was tested on a physical robot. - Not sim-to-real. A pass means the file is well formed for a simulator, not that a physical robot would behave the same. NIUA never runs a policy on hardware. - No physics fidelity score. Mass, inertia and joint limits are checked for being present and physically possible, not for matching a physical robot. The auto-sim run checks that a model loads and stays finite, nothing more. - World models are not built yet. The track is planned; no contract, check or receipt exists for it. - The robot numbers come from our own arm and test fixtures, not from customer results. ## The loop 1. Upload the file you built. REST: `POST /api/v1/upload` (multipart), which returns the key as `r2_key`; pass it as `artifact.key`. MCP: `create_upload` then `finalize_upload`, which returns `asset_key`; pass it as `key`. There is no URL artifact source; the file must be uploaded first. 2. Check it. REST: `POST /api/v1/check`. MCP: `check_artifact {key, contract}`. 3. If `verdict` is `fail`, read `failures[]`, fix the file in your tool, upload again, check again. 4. If `verdict` is `pass`, keep the `receipt`. Verify it with `GET /api/v1/receipts/{id}` or offline against the published key. ## Example session A real run on production (2026-10-15). Outcome: the street lantern passes `glb.prop@1` with receipt `05ae6dfd-a4a2-444b-95c0-13d317741c66`; `GET https://api.ohao.tech/api/v1/receipts/05ae6dfd-a4a2-444b-95c0-13d317741c66` returns `"valid": true`. Fixed before passing: glass transmission, triangle budget, collision shape. Full machine-readable record, outcome first (`outcome`), then every failure verbatim and the tool calls with timings: https://ohao.tech/example-session.json How the agent got there: 1. `get_contract glb.prop@1`. 2. Attempt 1: upload, `check_artifact` -> `fail` (check `e3689673-2ac1-462b-8ee2-459ed8bd59ee`). `failures[]`: `materials.glass_is_transmissive` (glass material has transmission 0), `budget.triangles` (24792 > 20000 per part), `collision.present` (no collision mesh). Repair: glass transmission set to 1, decimated to 19011 triangles. 3. Attempt 2: upload, `check_artifact` -> `fail` (check `4d433643-8e1b-405e-89b8-0154e6f3d9d0`). `failures[]`: `collision.present`. Repair: added a 12-triangle box collider named `UCX_d1_lantern`. 4. Attempt 3: upload, `check_artifact` -> `pass` (check `8f10aab0-4cec-4d0d-aee2-08775eb7198b`) with receipt `05ae6dfd-a4a2-444b-95c0-13d317741c66`. 5. Keep the receipt. Verify: `GET https://api.ohao.tech/api/v1/receipts/05ae6dfd-a4a2-444b-95c0-13d317741c66` returns `"valid": true`. ## Gate ### Check an artifact `POST /api/v1/check` ```json { "artifact": { "key": "" }, "contract": "glb.prop@1" } ``` `contract` is either a ref string (`id@version`) or an inline `niua.contract/v1` object. All `glb.*`, `wav.*`, `urdf.*` and `motion.*` ids are reserved for built-ins: an inline contract may use one only if it is identical to that built-in. An artifact over 100 MB returns 400 `invalid_request` with the limit before any check runs. Response: ```json { "check_id": "uuid", "verdict": "pass | fail", "artifact": { "key": "...", "sha256": "...", "bytes": 0, "kind": "glb | wav | urdf" }, "contract": { "id": "glb.prop", "version": 1, "sha256": "..." }, "checker": "niua-gate/1.0.0", "failures": [ { "rule": "collision.present", "severity": "error | warn", "path": "...", "subject": "...", "expected": "...", "got": "...", "fix": "..." } ], "metrics": {}, "receipt": { "...": "signed niua.receipt/v1 object, or null" }, "receipt_url": "string or null", "duration_ms": 0 } ``` `receipt` and `receipt_url` are present only when `verdict` is `pass`; otherwise they are null. Checks are free and rate-limited. `POST /api/v1/check` needs a full-scope API key: a read-scope key gets 403 `permission_denied` on any non-GET call. Match on `rule`, never on prose. - `GET /api/v1/checks?limit=` lists your recent checks. - `GET /api/v1/checks/{id}` returns one check. ### Contracts - `GET /api/v1/contracts` returns `{ "builtin": [{ref, sha256, contract}], "own": [...] }`. - `POST /api/v1/contracts` saves your own contract. Body is an inline `niua.contract/v1` object. All `glb.*`, `wav.*`, `urdf.*` and `motion.*` ids are reserved for built-ins, so save under your own namespace (for example `acme.chair`). Unknown rules and unknown fields are rejected. A contract needs at least one rule. A duplicate `id@version` returns 409 `conflict`; bump `version` instead. - `GET /api/v1/contracts/builtin` lists every built-in contract with its full rules and the sha256 a receipt binds (no auth). - `GET /api/v1/contracts/rules` is the public rule catalog (no auth). - MCP: `list_contracts`, `get_contract {id}`. Built-in contracts: `glb.prop@1`, `glb.prop-strict@1`, `wav.sfx@1`, `wav.voice@1`, `urdf.sim-ready@1`, `motion.g1-safe@1`, `splat.sim-ready@1`, `articulated.sim-ready@1`. `niua.contract/v1` shape: ```json { "schema": "niua.contract/v1", "id": "acme.chair", "version": 1, "kind": "glb", "title": "Acme chair", "rules": { "naming.no_default_names": {}, "parts.required": { "patterns": ["^frame$", "^seat$"] }, "budget.triangles": { "max": 20000 } } } ``` `schema`, `id`, `version`, `kind`, `title` (a string), and `rules` are all required. `kind` is `glb`, `wav`, `urdf` or `motion`. `rules` maps rule ids to their params; a rule that is absent is not enforced. `id` is 1 to 64 characters of `a-z`, `0-9`, `.`, `-`, `_`. `version` is an integer >= 1. GLB rules (`kind: "glb"`): - `artifact.parse`: always on for every kind; never list it in a contract. Fails on a file that does not parse or a file of the wrong kind for the contract. (A file over 100 MB never reaches the checker: the request fails with 400 `invalid_request`.) - `artifact.visual_geometry`: always on for GLB; never list it in a contract. The scene must contain visible triangle geometry (collision-only meshes do not count). - `naming.no_default_names`: every node and mesh has a real name, not an exporter default like Cube.001. - `parts.required`: each regex in `patterns` matches the name of at least one part. - `materials.assigned`: every primitive has a material. - `materials.glass_is_transmissive`: glass materials use KHR_materials_transmission with factor > 0. - `materials.metal_is_metallic`: metal materials have metallicFactor >= 0.5. - `budget.triangles`: total triangles <= `max`, and per node <= `per_part_max` when set. Collision-only triangles count toward `max` (reported as the `collision_triangles` metric) but not toward `per_part_max`. - `budget.texture_px`: every image's longer side <= `max` pixels. - `collision.present`: at least one collision mesh (`UCX_`, or `-colonly` for Godot). - `scale.bounds_m`: world bounding box in meters is within `[min, max]` per axis. The `max` limit applies to the bounds of visual and collision meshes together. - `transform.no_negative_scale`: no node has a mirrored transform. WAV rules (`kind: "wav"`): - `format.sample_rate`: sample rate is one of `allowed`. - `format.channels`: channel count is one of `allowed`. - `duration.range_s`: duration in seconds is within `[min, max]`. - `level.no_clipping`: sample peak stays below `max_dbfs`. - `level.loudness_lufs`: integrated loudness (EBU R128) is within `[min, max]` LUFS. - `silence.edges_ms`: leading and trailing silence each last <= `max_ms`. URDF rules (`kind: "urdf"`). Upload a zip holding one `.urdf` and the meshes it references (`application/zip`), or a bare `.urdf` that uses only box, cylinder and sphere geometry (`application/xml`). These are static checks at the zero joint configuration, simulation only, no hardware tested: they do not prove the values match a physical robot. - `artifact.parse`: always on. A bare URDF (root ``) or a zip with one `.urdf` (at most 4096 entries, 100 MiB unpacked); no xacro macros left. - `tree.valid`: one tree. Joints point at existing links, no link has two parent joints, exactly one root, no loops. - `inertial.mass_positive`: every link with geometry, or that is the child of a movable joint, has mass >= `min_kg`. Geometry-less frames on fixed joints (`world`, `tool0`) are exempt. - `inertial.tensor_valid`: principal moments all > 0, I1 + I2 >= I3, and none above mass x d^2 (d = model bounding-box diagonal; catches placeholder identity tensors). - `collision.on_visual_links`: every link with a `` has a ``; collision meshes have at most `max_mesh_triangles`. - `joints.limits`: revolute and prismatic joints have lower < upper, effort > 0 and velocity > 0; movable joints have a non-zero axis. - `meshes.resolve`: every `` (relative, or `package:///`) is in the upload and loads (STL, OBJ, COLLADA, GLB). Absolute paths and URLs never resolve. - `scale.metres`: the model's size is within `[min_m, max_m]` metres; a model about 1000x too large is reported as millimetres. Motion rules (`kind: "motion"`). Simulation only, no hardware tested. Upload one `niua.g1-motion/v1` CSV (REST `text/csv`; over MCP `create_upload` with content_type `text/plain`): a first line `# niua.g1-motion/v1 fps=<10..1000>`, a header naming `root_x`, `root_y`, `root_z`, `root_qw`, `root_qx`, `root_qy`, `root_qz` and the 29 Unitree G1 joints of the MuJoCo Menagerie model by name (any order), then one row per frame: metres, a unit quaternion with w first, radians. At most 60 s. The gate measures the clip in MuJoCo on CPU against the Menagerie `unitree_g1`; a pass's receipt records the MuJoCo version, timestep, integrator and controller gains. - `artifact.parse`: always on. The clip is readable in that format. - `simulation.unavailable`: always on. The server could not run MuJoCo; nothing about the clip was judged. Retry. - `joints.position_limits`: every joint inside the Menagerie model's range, by more than `margin_rad` never. - `joints.velocity_limits`: every joint's speed at most `max_ratio` times Unitree's published URDF velocity limit. - `contact.ground_penetration`: no robot geometry deeper than `max_m` into the floor. - `contact.self_collision`: no two links deeper than `max_depth_m` into each other (convex hulls; adjacent links excluded). - `feet.skate`: a foot contact point within `contact_height_m` of the floor slides at most `max_mps`. - `replay.upright`: replayed with joint PD tracking (the model's position actuators, no balance controller), the pelvis stays above `min_pelvis_m` and within `max_tilt_deg` of vertical for the whole clip. Strict: quasi-static clips pass, most dynamic ones do not. Read `GET /api/v1/contracts/rules` for the exact params of every rule. ### Receipts A passing check returns a signed `niua.receipt/v1`: ```json { "schema": "niua.receipt/v1", "receipt_id": "uuid", "artifact_sha256": "...", "contract": { "id": "glb.prop", "version": 1, "sha256": "..." }, "checker": "niua-gate/1.0.0", "verdict": "pass", "metrics": {}, "issued_at": "...", "key_id": "...", "signature": "base64" } ``` - `GET /api/v1/receipts/{id}` (public) returns `{ "receipt": {...}, "valid": true|false, "simulations": [...] }`. MCP: `verify_receipt {receipt_id}`, where `receipt_id` is the receipt's UUID. - `POST /api/v1/receipts/verify` with `{ "receipt": {...}, "artifact_sha256": "..." }` returns `{ "valid", "matches_artifact", "contract", "receipt" }`. `artifact_sha256` is optional; send it to confirm the receipt covers the exact file you hold. `matches_artifact` is a bool when you sent `artifact_sha256` and null otherwise. `contract` is `{ "ref", "sha256", "builtin" }`: `builtin` is true only when the receipt was checked against a contract published at `GET /api/v1/contracts/builtin`, byte for byte, so you can read the exact rules it met. `receipt` echoes the receipt the server verified. MCP: `verify_receipt {receipt, artifact_sha256?}`. Use this for a `check_sample` receipt: it is signed but not stored, so `GET` by id returns 404. - `GET https://api.ohao.tech/.well-known/niua-receipt-keys.json` (path `/.well-known/niua-receipt-keys.json` on the API host) is a JWKS (OKP, Ed25519). Pick the key whose `kid` equals the receipt's `key_id`. To verify offline: remove `signature` from the receipt and serialize the rest as canonical JSON: object keys sorted, no whitespace, strings JSON-escaped as in compact serde_json, integral numbers written without a decimal point, other numbers in shortest round-trip form. `signature` is standard base64 of the 64-byte Ed25519 signature over those bytes. The JWKS `x` is base64url (no padding) of the raw 32-byte public key. ### Simulate a passed robot After a URDF robot passes a `urdf.*` contract, `POST /api/v1/simulate` with `{ "receipt_id": "..." }` sweeps every limited joint through its range in MuJoCo on CPU (0, upper, lower, 0; the others at 0) and reports the deepest contact between bodies that are not parent and child. It returns `{ "simulation", "replay", "valid", "simulation_url" }`: - `simulation` is a signed `niua.sim-report/v1`: `receipt_id` and `artifact_sha256` bind it to the receipt; `joints[]` has each joint's `lower`, `upper`, `max_penetration_m`, `worst {bodies, at}` and `collides`; `summary` has `colliding_joints` and `collision_free`; `simulator` names MuJoCo's version and `tolerance_m`. It verifies exactly like a receipt. The receipt does not change. - `replay` (`niua.urdf-sweep-replay/v1`) is every joint's value at every sample (`hz`, `joints`, `frames`); SHA-256 of its canonical JSON equals the report's `replay_sha256`. - A colliding joint: tighten its `` or the colliders, check again, simulate again. - `GET /api/v1/simulations/{sim_id}` (public) reads it back with `valid`. Owner only to create; `400` for a non-robot or failed receipt, `503` if the server has no simulator. - MCP: `simulate_artifact {receipt_id}` (upload a URDF zip with `create_upload` using `application/zip`, then `check_artifact`). CLI: `niua-runner simulate receipt.json`. - Try it without an account: `POST /api/v1/playground/samples/robot-arm/simulate` (nothing stored), or in the browser: https://ohao.tech/niua/playground?starter=robot-arm, pass the arm, then "Sweep the joints in MuJoCo" plays the sweep in 3D with a slider per joint. Kinematic range of motion only: collision geometry, no gravity, no actuators, no dynamics. Not physics fidelity, not sim-to-real. Simulation only, no hardware tested. ### Repair a G1 clip that falls over A generated Unitree G1 clip often fails `motion.g1-safe@1` on `replay.upright`: the robot falls over in the gate's MuJoCo replay. `POST /api/v1/repair` with `{ "artifact": { "key": "" } }` runs the clip in that same replay (same model, controller, timestep and frame interpolation) with balance added on the ankles, and records what the simulation actually did. No external forces. It returns `{ "summary", "faithful", "output", "repair", "next" }`: - `summary` is one plain sentence on what happened. `faithful` is true when it is the same motion, balanced: no frame blended toward standing and no joint moved more than 0.2 rad. - `output` is the repaired clip, stored as your upload: `{ asset_id, key, sha256, bytes, url }`. Its first line claims the repair: `# niua.g1-motion/v1 fps=30 repaired_by=niua.motion-repair@1 source_sha256=`. - `repair` is a signed `niua.motion-repair/v1`: `source_sha256`, `output_sha256`, `changes` (root lift and drift, frames blended, clamps, and per joint the largest and RMS change in rad), `before` and `after` (the numbers the contract judges), `faithful`, `summary`. It verifies like a receipt. - `next` is the call to make: `POST /api/v1/check` on `output.key` with `motion.g1-safe@1`. A repair is not a verdict. The gate checks the repaired clip with the same thresholds, and a pass receipt carries `metrics.provenance` `{ repaired_by, source_sha256, basis }`: what the clip's first line claims. Anyone can write that line, so keep the signed `repair` document too: its `output_sha256` equals the receipt's `artifact_sha256`, and that pair is the proof. - What works today: idle and standing clips repair faithfully and pass. Generated walking does not: in the lab a sampling repair keeps it upright, but the feet still slide 1.4 to 4.0 m/s in the simulation itself, the walk shortens, and the gate replay has no feedback, so any pass would not survive 1e-4 rad of noise. `summary` says "Not repaired" and the check fails. Passing walking honestly needs a closed-loop controller in the replay (a new contract version, not built). - MCP: `repair_motion {key}`. CLI: `niua-runner repair clip.csv` (repairs, checks the result, verifies both signed documents locally; `--json` for one JSON object). - Try it without an account, on 4 s of real NVIDIA MotionBricks output: `POST /api/v1/playground/samples/g1-motionbricks-idle/check?contract=motion.g1-safe@1&variant=raw` fails on `replay.upright`; `POST /api/v1/playground/samples/g1-motionbricks-idle/repair` returns the signed repair document and the clip (nothing stored); `.../check?contract=motion.g1-safe@1&variant=fixed` passes with a signed receipt whose `metrics.provenance` names the repair and the raw clip's sha256. - `400` for a file that is not a `niua.g1-motion/v1` clip, not UTF-8, or over 8 MiB or 30 s of motion (split it); `402` `quota_exceeded` if storing the output would pass your upload quota; `503` if the server has no repair (`NIUA_SIM_REPAIR_SCRIPT`). Free, rate-limited like checks. Simulation only, no hardware tested. ### Check a splat scene before it becomes a sim environment Zip three files and upload the zip as `application/zip`: `scene.ply` (a 3D Gaussian PLY: x, y, z, opacity, scale_0..2, rot_0..3; binary or ascii), `collider.obj` (each `o`/`g` group one convex collision part; or `.stl`) and optionally `niua-splat.json` `{"up_axis": "+z", "units": "m", "reference": {"from": [x,y,z], "to": [x,y,z], "metres": d}}`. Then `POST /api/v1/check` with `splat.sim-ready@1` (MCP: `check_artifact {key, contract: "splat.sim-ready@1"}`; CLI: `niua-runner check scene.zip --contract splat.sim-ready@1`). - `frame.matches_collider` fails when the splat and the collider disagree (Y-up against Z-up, centimetres, an offset). The fix is the transform that makes them agree, `p' = scale × R(q) × p + t`, with axis-aligned rotations and unit conversions reported exactly, and the median distance after it. - `frame.gravity_aligned` fails when the floor is not level against `up_axis`; the fix is a quaternion for both files. - `collision.drop_test` drops 10 cm boxes in MuJoCo. MuJoCo collides each collision part as its convex hull, so a whole room as one concave mesh fails here: split it into convex parts. - The receipt's `metrics.scale.metric_scale` says `consistent with the declared reference measurement` (the reference is the uploader's, not verified) or `plausible only`, and `metrics.not_judged` names rendering quality, relighting, contact and friction accuracy. - SPZ and glTF `KHR_gaussian_splatting` are not read yet: export the splat to PLY. ## Trace Optional headers on any request tell NIUA which agent run a call belongs to: - `X-NIUA-Client`: client name and version, for example `my-agent/1.4`. - `X-NIUA-Session`: any stable id for one agent run, at most 128 characters. Use the same value on every call in the run. - `X-NIUA-Tool`: the tool or step making the call. Read traces back: - `GET /api/v1/traces/sessions` lists sessions. - `GET /api/v1/traces/sessions/{session_key}/calls` lists the calls in one session. Use the `session_key` returned by `GET /api/v1/traces/sessions` and URL-encode it. Any `X-NIUA-Session` value groups calls under session_key `mcp:` (the hosted MCP server sends a 32-character hash of its own session id in that header). Without the header, calls group per API key per UTC day as `key::`, and browser calls as `web:`. - `GET /api/v1/traces/stream` is a live SSE stream of your calls (authenticated). ## Budgets Each API key can carry a spend limit so a runaway agent cannot drain the wallet. - `PATCH /api/keys/{key_id}/limit` with `{ "spend_limit_cents": <0..2147483647>, "spend_period": "day" | "month" | "total" }`. Send both fields as `null` to clear the limit. - Managing limits requires a browser session. An API key calling this route gets 403 `permission_denied`. - A paid call that would exceed the limit returns HTTP 402 with `error.code` = `spend_limit_exceeded`, plus top-level `limit_cents`, `spent_cents`, `period`, and `price_cents`. Do not retry; ask a human to raise the limit. - A revoked key returns 401 `invalid_api_key`. Checks are free and never count against a budget. ## Media tools Legacy paid generation (the hosted add-on in Generation above), for agents that need a starting file to check. Prices are never listed here: read them from `GET /api/v1/models` (per-model `cost`) or `GET /api/billing/prices`. A wallet is charged at submission and refunded on failure. - `POST /api/v1/generate/`: `music`, `sfx`, `speech`, `voicegen`, `dialogue`, `mesh`, `texture`, `motion`, `text2motion`, `rig`. NIUA does not generate images; upload a still, then call `mesh` with `image_key`. - `POST /api/v1/retarget`: retarget a BVH clip onto `mixamo` or `ue5`. - `POST /api/v1/mesh-decompose`: split an owned mesh into named watertight parts. Unavailable in the EU, UK, and South Korea (HTTP 451 before any charge). - `GET/POST /api/v1/voices`: voice library for speech cloning. - `GET /api/v1/jobs/{id}`, `GET /api/v1/jobs`: job status and history. Generation is async: you get `status: "processing"` and a `job_id`. On MCP, call `wait_for_job` or `wait_for_jobs` instead of polling. - `GET /api/v1/assets`, `GET /api/v1/assets/{job_id}/files`, `GET /api/v1/assets/download?key=|job_id=`: your library and presigned downloads. - `GET /api/v1/projects`, `POST /api/v1/projects`: folders for assets. - `GET /api/v1/models`: the catalog (public ids, live health, price, params). - `POST /api/v1/upload`: multipart upload; returns the key to pass to `check` or to a generation call. Auto-rig may be offline; read its `status` in `GET /api/v1/models` before calling. ## Auth - API keys: `Authorization: Bearer niua_live_...` (or `niua_test_...`). Create and revoke at https://ohao.tech/niua/console/keys. `X-API-Key: ` is accepted as an alias. Keep keys server-side. - The web app uses a short-lived session token. Agents should use an API key. - MCP: OAuth sign-in on first connect, or a Bearer `niua_live_` key. Errors are uniform `{ "error": { "code", "message", "doc_url" } }`. Match on `code`. Every code, what it means and what to do next: https://ohao.tech/niua/docs/errors (JSON: https://ohao.tech/errors.json); each `doc_url` links its section. Stable codes include `invalid_api_key` (401), `permission_denied` (403), `insufficient_funds` (402), `spend_limit_exceeded` (402), `invalid_request` (400), `conflict` (409), and `not_found` (404). A wallet that cannot cover a paid call returns 402 `insufficient_funds`. Send `Idempotency-Key: ` on mutating calls; replays return the original response for 24 hours. ## MCP Hosted remote MCP server, version 0.3.0: https://mcp.ohao.tech/mcp (streamable HTTP). Gate tools: `check_artifact {key, contract}`, `list_contracts`, `get_contract {id}`, `verify_receipt {receipt_id}` or `verify_receipt {receipt}` (the whole receipt object, for a `check_sample` receipt), `list_samples`, `check_sample {id, contract, variant}` (NIUA's sample files, nothing to upload). Upload tools: `create_upload`, `finalize_upload`; the upload is an HTTP PUT of the file bytes, so it needs a client that can make HTTP requests (Claude Code, Cursor, Codex). A chat client without one can still run `list_samples` and `check_sample`. Media tools mirror the REST generation endpoints (the legacy paid add-on). MCP errors keep the REST error contract. A failed gateway call returns `isError` with the human message, then a JSON block `{ "error": { "status", "code", "message", "doc_url", "retry_after_s", "next", ... } }` (the same object is in `structuredContent`). Match on `code`; `retry_after_s` says how long to wait on a 429; `next` is the tool's next step. ## Resources - [Full reference](https://ohao.tech/llms-full.txt): every gate endpoint, receipt verification, and the media tools in detail. - [OpenAPI spec](https://api.ohao.tech/openapi.yaml): canonical machine-readable contract. - [Interactive docs](https://ohao.tech/niua/docs/api): the spec rendered with Try-It-Now.