BookNest's display for The Clockmaker's Paradox needs a mantel clock. This script spends 30 credits on a preview remeshed to about 20,000 faces and sized to real-world height (auto_size), a PBR refine and the GLB download:
// meshy-prop.mjs: text to 3D in two paid stages, preview (mesh) then refine (texture)
import fs from 'node:fs';
const API = 'https://api.meshy.ai/openapi/v2/text-to-3d';
const headers = { Authorization: `Bearer ${process.env.MESHY_API_KEY}`,
'Content-Type': 'application/json' };
async function run(body) { // create a task, then poll it
const res = await fetch(API, { method: 'POST', headers, body: JSON.stringify(body) });
if (!res.ok) throw new Error(`${body.mode}: ${res.status} ${(await res.json()).message}`);
const { result: id } = await res.json();
for (;;) {
const task = await (await fetch(`${API}/${id}`, { headers })).json();
if (task.status === 'SUCCEEDED') return task;
if (task.status === 'FAILED') throw new Error(task.task_error.message);
await new Promise((resolve) => setTimeout(resolve, 5000));
}
}
try {
const preview = await run({ mode: 'preview', ai_model: 'latest', target_formats: ['glb'],
prompt: 'An antique walnut mantel clock with a brass bezel, for a bookshop shelf',
should_remesh: true, target_polycount: 20000, auto_size: true }); // 20 credits
const refine = await run({ mode: 'refine', preview_task_id: preview.id,
enable_pbr: true, target_formats: ['glb'] }); // 10 credits
const glb = await fetch(refine.model_urls.glb); // signed URL; assets kept 3 days
fs.writeFileSync('clock.glb', Buffer.from(await glb.arrayBuffer()));
console.log('clock.glb', preview.consumed_credits + refine.consumed_credits, 'credits');
} catch (err) {
console.error(err.message);
}It is shown not run here past its first request: this book buys no Meshy 20,231 credits. Run with the retired test key and with none, it printed the API's real answers:
preview: 401 Invalid API key preview: 401 Missing API key
A DELETE on the task URL cancels and refunds only a PENDING task; once a task is IN_PROGRESS, its credits are spent, so set a monthly credit limit on each key.
<!doctype html>
<style>
body { margin: 0; padding: 8px; background: #fafaf7; font: 12px system-ui, sans-serif; color: #263238; }
canvas { display: block; max-width: 100%; }
pre { margin: 4px 0; font-size: 11px; background: #1d2327; color: #d7f0dd; padding: 6px; max-width: 580px; white-space: pre-wrap; }
</style>
<script type="importmap">
{ "imports": {
"three": "https://cdn.jsdelivr.net/npm/three@0.186.1/build/three.module.js",
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.186.1/examples/jsm/" } }
</script>
<pre id="log"></pre>
<script type="module">
import * as THREE from 'three';
import { GLTFExporter } from 'three/addons/exporters/GLTFExporter.js';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
const log = (s) => { document.getElementById('log').textContent += s + '\n'; };
// What meshy-prop.mjs would do: preview (20 credits) → refine (10 credits) → fetch model_urls.glb.
// Here the "service" is a function that builds the clock and packs it as binary glTF in memory.
function fakeMeshyTask() {
const clock = new THREE.Group();
const wood = new THREE.MeshStandardMaterial({ color: '#5a3522', roughness: 0.45, name: 'walnut' });
const brass = new THREE.MeshStandardMaterial({ color: '#c9a44c', metalness: 1, roughness: 0.3, name: 'brass' });
const face = new THREE.MeshStandardMaterial({ color: '#fff8e8', roughness: 0.6, name: 'dial' });
const body = new THREE.Mesh(new THREE.BoxGeometry(0.24, 0.16, 0.09), wood); // metres: auto_size to real height
const dome = new THREE.Mesh(new THREE.CylinderGeometry(0.1, 0.1, 0.09, 40, 1, false, Math.PI / 2, Math.PI), wood);
dome.rotation.x = Math.PI / 2; dome.position.y = 0.08;
const bezel = new THREE.Mesh(new THREE.TorusGeometry(0.068, 0.008, 12, 40), brass); bezel.position.set(0, 0.07, 0.046);
const dial = new THREE.Mesh(new THREE.CircleGeometry(0.066, 40), face); dial.position.set(0, 0.07, 0.0455);
const base = new THREE.Mesh(new THREE.BoxGeometry(0.27, 0.02, 0.11), wood); base.position.y = -0.09;
clock.add(body, dome, bezel, dial, base);
clock.name = 'mantel-clock';
return new GLTFExporter().parseAsync(clock, { binary: true });
}
log('POST /openapi/v2/text-to-3d {mode: "preview", should_remesh: true, target_polycount: 20000, auto_size: true}');
log(' … SUCCEEDED (20 credits)');
log('POST {mode: "refine", preview_task_id, enable_pbr: true} … SUCCEEDED (10 credits)');
const glb = await fakeMeshyTask();
log(`GET model_urls.glb → clock.glb, ${(glb.byteLength / 1024).toFixed(1)} KB (an ordinary glTF 2.0 binary)`);
const gltf = await new GLTFLoader().parseAsync(glb, '');
let vertices = 0;
gltf.scene.traverse((o) => { if (o.isMesh) vertices += o.geometry.attributes.position.count; });
const names = new Set();
gltf.scene.traverse((o) => { if (o.isMesh) names.add(o.material.name); });
log(`GLTFLoader: ${gltf.scene.children[0].children.length} meshes, ${vertices} vertices, materials ${[...names].join(', ')}`);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
renderer.setSize(600, 220);
document.body.appendChild(renderer.domElement);
const scene = new THREE.Scene();
scene.background = new THREE.Color('#f3efe6');
scene.add(new THREE.HemisphereLight('#ffffff', '#8d6e63', 1.4));
const sun = new THREE.DirectionalLight('#ffffff', 2.5); sun.position.set(1, 2, 3); scene.add(sun);
const camera = new THREE.PerspectiveCamera(30, 600 / 220, 0.01, 10);
camera.position.set(0, 0.06, 0.75);
scene.add(gltf.scene); // the "download", drop-in
renderer.setAnimationLoop((t) => { gltf.scene.rotation.y = t / 1500; renderer.render(scene, camera); });
</script>