Rupa3D API — local physics + licensed generative router

This service accepts a buyer-owned GLB and returns an audited GLB, a measured single-hull or VHACD convex-decomposition collider, or a ready-for-physics GLB with receiver-verifiable Proof Profile v1 evidence. It also accepts a binary PNG/JPEG and routes image-to-3D through the only model currently licensed for paid use, TripoSR. A technical preview is available at https://api.migan3d.com; the MCP endpoint is https://mcp.migan3d.com/mcp. API access requires an authorized key. Public checkout, Polar billing, RunPod generation, and text-to-3D are not enabled on this preview. The GLB audit path has been verified over HTTPS; the other operations described below remain library capabilities and do not imply a live generative provider.

Quick start (local credit rail)

Node 22.14 or newer is required because this slice uses experimental node:sqlite. No package was added.

$env:RUPA3D_API_DATA = 'C:\safe\rupa3d-api-data'
$env:RUPA3D_HMAC_SECRET = '<generate and store outside the repository>'
$env:RUPA3D_VERIFIER_COMMIT = '<40-hex commit containing this verifier>'
$env:RUPA3D_RUNPOD_KEY = '<store outside the repository>'
$env:RUPA3D_RUNPOD_TRIPOSR_ENDPOINT = '<configured RunPod endpoint base URL>'
node api/admin.mjs beri '<buyer-key>' 50
node api/jalan.mjs

Image-to-3D uses binary upload only; the server never fetches a buyer URL:

curl -H "Authorization: Bearer $RUPA3D_KEY" \
  -H "Content-Type: application/octet-stream" \
  --data-binary @reference.png http://127.0.0.1:8787/v1/gambar

curl -H "Authorization: Bearer $RUPA3D_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: buyer-image-0001" \
  -d '{"gambar_id":"gambar_...","operasi":"gambar_ke_3d","model":"triposr"}' \
  http://127.0.0.1:8787/v1/job

model-generatif.json is a closed registry. TripoSR is enabled. Step1X-3D, Hi3DGen, and TRELLIS.2 are recorded with their blocking dependency and cannot be selected by a credited job, regardless of request parameters. Endpoints are looked up by environment-variable name from the registry; values and the RunPod key are never stored in repository files.

Upload and process:

curl -H "Authorization: Bearer $RUPA3D_KEY" \
  -H "Content-Type: application/octet-stream" \
  --data-binary @model.glb http://127.0.0.1:8787/v1/aset

curl -H "Authorization: Bearer $RUPA3D_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: buyer-order-0001" \
  -d '{"aset_id":"aset_...","operasi":"siap_fisika","parameter":{"meter_per_unit":1,"kerapatan_kg_m3":1000}}' \
  http://127.0.0.1:8787/v1/job

The other accepted scale bases are dimensi_diketahui: {"sumbu":"x|y|z","meter":2.4} and a convincing human-scale kelas. The service never guesses scale.

The mass basis is exactly one of three inputs:

- In every case the stated mass is recorded under asset.extras.rupa3d.fisika_dinyatakan with terukur: false. It is never issued as an api/massa claim: that claim would only repeat the input, and the Proof Profile would lose where the number came from. - Closed mesh: the service measures the volume and derives density = massa_kg / volume. Inertia and pose are then issued as receiver-verifiable claims whose metode names this derivation. - Open or inward-wound mesh: only the statement is recorded.

Optional kelas_benda (for example "drum_baja_200_liter") adds laporan.saran_massa: a sourced mass range for that object class. It is only offered when both hold:

It is a suggestion that needs confirmation. It is never copied to massa_kg, never embedded in the GLB, and never a claim. When massa_kg is also given, massa_dinyatakan_di_luar_rentang flags a stated mass outside the range. Currently 15 of 41 researched classes qualify. api/saran-massa.json is derived from riset/massa-kelas/ by node api/bangun-saran-massa.mjs; do not edit it by hand.

collider and siap_fisika accept mode: "cembung" | "dekomposisi" | "otomatis". The default is otomatis: measure one hull first, and run VHACD only when that hull is marked (including volume-hull-berlebih). Optional parameter_vhacd records/controls resolution, concavity, maxConvexHulls, alpha, beta, both downsampling values, and convexHullApproximation; defaults are defined once in dekomposisi-cembung.mjs. Volume-union sampling uses fixed sampel_volume=65536 and seed_volume=1381322817 unless supplied.

MCP Streamable HTTP

Point an MCP client at http://127.0.0.1:8787/mcp and send the same Authorization: Bearer <key> header. The tool is rupa_aset_siap_fisika; it accepts glb_base64, operasi, parameter, and idempotency_key. rupa_gambar_ke_3d accepts gambar_base64, optional model: "triposr", and idempotency_key. Both use the same reservations, ownership, limits, and job implementation as REST. While text-to-3D is enabled and ready, rupa_teks_ke_3d (teks, optional pengarang, idempotency_key) returns a running job and rupa_job (job_id) reports its status and URLs.

The HTTP MCP transport accepts at most 100 JSON-RPC messages per batch. A batch of 101 or more returns HTTP 400 with JSON-RPC code -32600 before tool execution. This security limit comes from the pinned MCP SDK. The existing Rupa3D request-byte limit still applies; this does not introduce the SDK body-parser limit because Rupa3D passes an already parsed body.

Text-to-3D (teks_ke_3d, disabled by default)

A short brief is written into a Kiln program by DeepSeek-V4.1-Flash (MIT weights, official API; the only author that passed preregistered benchmark B-PENULIS-1), rendered by Kiln 0.8.0, audited by Rupa3D, and returned with the program that rebuilds it. Design, threat model, and adopted review findings: docs/RANCANGAN-TEKS-KE-3D.md.

curl -X POST http://127.0.0.1:8787/v1/job -H "Authorization: Bearer $KEY" \
  -H "Idempotency-Key: text-crate-0001" -H "Content-Type: application/json" \
  -d '{"operasi":"teks_ke_3d","teks":"a small wooden crate"}'   # 202, status berjalan
curl http://127.0.0.1:8787/v1/job/<id> -H "Authorization: Bearer $KEY"   # poll until sukses|gagal

api/audit-kotak-batas and api/teks/jumlah-segitiga (both required), program.kiln.js, and lembar.png. Provenance at asset.extras.rupa3d.generatif_teks records the author, Kiln version and runtime identity, SHA-256 of program, raw GLB, and brief, and Kiln QA. - bangun_ulang.status: terbukti-di-server means the server re-rendered the unedited program and hashed the result itself. That result must match the raw GLB on the same Kiln and runtime identity; cross-OS equality is not yet measured.

contain errors or omissions, and are for reference only. The brief is sent to DeepSeek, which processes data in the PRC. Do not put confidential or personal data in a brief.

- A per-job budget proxy holds the provider key; Kiln only gets a per-job token. - Caps are $0.60 per job and $5 per UTC day by default. Refusals return 402, which is not retried. - Queue is 1 global and 1 per owner; the deadline is 1,800 s. - A failed or over-budget job releases its credits. - Production requires Kiln's Linux isolated evaluator; the feature stays closed unless a readiness probe passes in the configured mode.

Enable only after the price and buyer gate are approved (env names in deploy/env.example; values never enter the repository).

Credits and settlement

OperationCredits
audit1
collider3
siap_fisika5
gambar_ke_3d3
teks_ke_3d10 (proposal; disabled)

The generative price is derived from the stated A40 rate and observed timing: $1.22 / 3600 = $0.0003389/s; a warm 49–63 second job costs $0.01660–$0.02135, while the conservative 60 second cold start plus 63 second job costs $0.04168. One $9/50 = $0.18 credit is about 4.32 times that worst stated GPU cost, leaving room for failed-provider attempts, verification, storage, and transfer. A failed Rupa3D job releases the held credit.

The local rail uses SQLite hold -> settle | release. The Polar adapter asks Polar for the meter balance, subtracts this customer's active local reservations, and only ingests one usage event after a job succeeds. The event uses external_id = job id. A post-success delivery failure becomes a local reconciliation debt; retry keeps the same external ID. Raw keys are never stored or written to application logs.

Proof and limits

The output stores Proof Profile v1 at asset.extras.rupa3d.bukti and the collider contract at asset.extras.rupa3d.collider. A receiver should call hitungUlang(claim, { jalur_glb }); the canonical hash excludes the Rupa3D namespace, avoids self-reference, and still changes when one BIN byte changes.

triangles, and node depth 256. The server never fetches user URLs (no SSRF).

most 10 MiB and 4096×4096 pixels read from the file header. Images are owned by the caller and expire after 24 hours, like GLB assets.

under one deadline. It requires output.mesh_base64, validates a real GLB with the same raw limits as uploaded assets, and fails closed otherwise.

receiver-recomputable world bounding box in source units and triangle count. Mass is never inferred: even a closed mesh still needs physical scale and density, while an open mesh is rejected independently.

asset.extras.rupa3d.generatif: model, provider-reported version/commit (or an explicit not-reported marker), closed runtime license path, source-image SHA-256, canonical output fingerprint, provider job id, and timestamp.

old-space limit. The in-process runner exists only for deterministic tests.

process-health response and no storage, customer, credit, or build data.

with the same time and memory limits (recomputation re-runs VHACD, Rapier hulls, pose, and mesh integration, which must never stall the HTTP loop). Credits settle only when the record itself says every claim passed. The append-only audit/verifikasi-job.jsonl gets one line per verified job, pass or fail — including timeouts — with the input SHA-256, canonical output fingerprint, verifier commit, and every claim verdict. The 24-hour sweeper never removes this hash/verdict trail, and backups contain it but never buyer geometry.

for marked concave objects. Each public Compound part stores its transformed vertices and indices. Verification measures mesh-to-union, sampled union-surface-to-mesh, and Monte Carlo union volume with its standard error.

constant. Surface sampling is finite, and Monte Carlo volume is an estimate.

16..256. Its maxConvexHulls=1 was observed not to cap output; Rupa3D enforces that negative control as one hull and marks the concave result.

- Closure is welded in each primitive's local space. - Overlapping closed primitives are not a CSG union and can double-count mass. - An open or inward-wound primitive does not fail the job: mass, inertia and pose are omitted, and the report names the primitive (alasan_massa_tidak_tersedia).

- Translation keyframes are scaled with the mesh. - Collider, mass, inertia and pose are measured on the default pose (the node's default TRS). The collider does not follow moving parts, and the report says so. - Skins and morph targets change the mesh itself and are rejected with HTTP 422 mesh_tidak_statis.

control, sized to the asset: - the floor is at least 20× the diagonal; - the drop starts 2 % of the diagonal above the lowest point; - the test center of mass sits half a diagonal beyond a support edge.

audit/verifikasi-job.jsonl, and its reserved credits are released.

signatures, revocation, or identity.

Run offline tests with:

node --test --experimental-test-isolation=none api/test-api.mjs api/test-situs-deploy.mjs api/test-generatif.mjs

The child-process case is visibly marked and requires RUPA3D_UJI_PROSES_ANAK=1 outside a sandbox that permits spawning.

Product site, measured examples, and deploy package

api/situs/ is a dependency-free static product and documentation site. Its checkout configuration is api/situs/checkout-config.json; empty channel values deliberately render a “Coming soon” state. Do not add placeholder payment URLs.

Regenerate repository-owned table/chair evidence and receiver verification:

node api/contoh/buat-contoh.mjs
node api/contoh/hitung-ulang.mjs api/contoh/hasil/kursi-siap-fisika.glb

Render the two PNG comparisons with the dedicated Blender script, regenerate documentation from this README plus openapi.yaml, and serve locally:

blender --background --python bpy/render_api_examples.py -- api/contoh/hasil/render.json api/situs/aset
node api/situs/bangun-docs.mjs
node api/situs/serve.mjs

api/deploy/ contains PM2/systemd units, nginx, an env-name-only template, backup script, and a canary/smoke/rollback runbook. The technical preview uses an isolated service account and a persistent server-managed HMAC. Checkout remains disabled; the local instructions above do not require access to the hosted service.

OpenAPI contract

The exact repository contract is included below.

openapi: 3.1.0
info:
  title: Rupa3D Physics and Licensed Generative API
  version: 1.2.0-local
  description: >-
    Technical preview is available with authorized API keys. Public checkout, Polar billing, and RunPod generation are not enabled. The text-to-3D
    operation teks_ke_3d is disabled by default (503 fitur_mati) until its price and the buyer gate are approved.
servers:
  - url: https://api.migan3d.com
    description: Technical preview; authorized key required
  - url: http://127.0.0.1:8787
    description: Local development
security:
  - bearerAuth: []
paths:
  /v1/sehat:
    get:
      security: []
      summary: Minimal unauthenticated health check
      responses:
        '200': { description: Service process is accepting HTTP requests }
  /v1/aset:
    post:
      summary: Upload one GLB without asking the server to fetch a URL
      requestBody:
        required: true
        content:
          application/octet-stream:
            schema: { type: string, contentMediaType: model/gltf-binary, contentEncoding: binary }
      responses:
        '201': { description: Asset stored for 24 hours }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '413': { description: Body exceeds 25 MiB }
        '422': { description: Body is not a GLB }
  /v1/gambar:
    post:
      summary: Upload one PNG/JPEG without asking the server to fetch a URL
      requestBody:
        required: true
        content:
          application/octet-stream:
            schema: { type: string, contentEncoding: binary }
      responses:
        '201': { description: Magic-checked image stored for its owner for 24 hours }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '413': { description: Body exceeds 10 MiB or dimensions exceed 4096 by 4096 }
        '422': { description: Body is not a structurally recognized PNG/JPEG header }
  /v1/job:
    post:
      summary: Run GLB processing, licensed image-to-3D generation, or asynchronous text-to-3D (disabled by default)
      parameters:
        - in: header
          name: Idempotency-Key
          required: true
          schema: { type: string, minLength: 8, maxLength: 200 }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              properties:
                aset_id: { type: string }
                gambar_id: { type: string }
                operasi: { enum: [audit, collider, siap_fisika, gambar_ke_3d, teks_ke_3d] }
                teks:
                  type: string
                  minLength: 3
                  maxLength: 600
                  description: >-
                    teks_ke_3d brief, 3..600 code points after whitespace is collapsed. Rejected when it contains
                    control, zero-width, bidi, or Unicode tag characters, or starts with '-'. The brief is sent to
                    DeepSeek (data processed in the PRC); do not include confidential or personal data.
                pengarang:
                  enum: [deepseek-v4.1-flash]
                  default: deepseek-v4.1-flash
                  description: Paid author registry currently permits DeepSeek-V4.1-Flash only (MIT weights, official API).
                model:
                  enum: [triposr]
                  default: triposr
                  description: Paid registry currently permits TripoSR only.
                parameter:
                  type: object
                  description: Collider controls; siap_fisika additionally requires one scale basis and density or a material preset.
                  properties:
                    mode:
                      enum: [cembung, dekomposisi, otomatis]
                      default: otomatis
                      description: Automatic keeps a passing single hull, otherwise runs VHACD.
                    arah: { type: integer, minimum: 8, maximum: 4096, default: 96 }
                    ambang_persen_diagonal:
                      type: number
                      exclusiveMinimum: 0
                      maximum: 10
                      default: 0.5
                      description: Rupa3D policy threshold, not a derived physical constant.
                    parameter_vhacd:
                      type: object
                      properties:
                        alpha: { type: number, minimum: 0, maximum: 1, default: 0.05 }
                        beta: { type: number, minimum: 0, maximum: 1, default: 0.05 }
                        concavity: { type: number, minimum: 0, maximum: 1, default: 0.01 }
                        planeDownsampling: { type: integer, minimum: 1, maximum: 32, default: 4 }
                        convexHullDownsampling: { type: integer, minimum: 1, maximum: 32, default: 4 }
                        maxConvexHulls: { type: integer, minimum: 1, maximum: 256, default: 32 }
                        resolution:
                          type: integer
                          minimum: 16
                          maximum: 256
                          default: 128
                          description: Guarded at 256 because larger values trapped Rapier 0.20 WASM in local probes.
                        convexHullApproximation: { type: boolean, default: true }
                      additionalProperties: false
                    sampel_volume: { type: integer, minimum: 4096, maximum: 1000000, default: 65536 }
                    seed_volume: { type: integer, minimum: 0, maximum: 4294967295, default: 1381322817 }
                    meter_per_unit: { type: number, exclusiveMinimum: 0 }
                    dimensi_diketahui: { type: object }
                    kelas: {}
                    kerapatan_kg_m3: { type: number, exclusiveMinimum: 0, description: Mass basis 1 of 3 — give exactly one of kerapatan_kg_m3, bahan, massa_kg. }
                    bahan: { type: string, description: Mass basis 2 of 3 — cited preset material. }
                    massa_kg:
                      type: number
                      exclusiveMinimum: 0
                      description: >-
                        Mass basis 3 of 3 — mass stated by the buyer. Always stored as
                        asset.extras.rupa3d.fisika_dinyatakan (terukur false) and never issued as an api/massa
                        claim. On a closed mesh the measured volume gives density = massa_kg / volume, and inertia
                        and pose are issued as verifiable claims whose metode names that derivation.
                    kelas_benda:
                      type: string
                      pattern: '^[a-z0-9_]{2,60}$'
                      description: >-
                        Optional object class (e.g. drum_baja_200_liter) for laporan.saran_massa — a sourced mass
                        range, offered only for narrow classes (max/min <= 3) whose reference dimensions match the
                        measured box within 1.5x. A suggestion requiring confirmation; never copied to massa_kg,
                        never in the GLB, never a claim.
              oneOf:
                - required: [aset_id, operasi, parameter]
                  properties:
                    operasi: { enum: [audit, collider, siap_fisika] }
                - required: [gambar_id, operasi]
                  properties:
                    operasi: { const: gambar_ke_3d }
                - required: [teks, operasi]
                  properties:
                    operasi: { const: teks_ke_3d }
      responses:
        '200': { description: Existing or newly completed idempotent job (terminal status sukses or gagal) }
        '202':
          description: >-
            Accepted and not yet terminal (teks_ke_3d). Status moves menahan -> berjalan -> menyelesaikan ->
            sukses|gagal; poll GET /v1/job/{id}. Output is AI-generated and may contain errors or omissions.
        '401': { $ref: '#/components/responses/Unauthorized' }
        '402': { description: Insufficient available credits after active reservations }
        '403': { description: teks_ke_3d in owner-only local mode and the key is not the owner's }
        '409': { description: Idempotency-Key already used for a different teks_ke_3d brief }
        '422': { description: Invalid operation, scale basis, density, model license gate, image, GLB, or brief }
        '429': { description: Per-key rate limit exceeded, or the teks_ke_3d queue is full }
        '503': { description: teks_ke_3d disabled or not ready (fitur_mati), or the daily generation budget is spent }
  /v1/job/{id}:
    get:
      summary: Read job status owned by the authenticated customer
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: string }
      responses:
        '200': { description: Job status }
        '404': { description: Job not found for this customer }
  /v1/job/{id}/hasil.glb:
    get:
      summary: Download a successful job's GLB
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: string }
      responses:
        '200':
          description: GLB with embedded Rupa3D proof
          content:
            model/gltf-binary:
              schema: { type: string, contentEncoding: binary }
        '404': { description: Result unavailable or not owned by this customer }
  /v1/job/{id}/program.kiln.js:
    get:
      summary: Download the Kiln program that rebuilds a successful teks_ke_3d job's raw GLB
      description: >-
        Served as a plain-text attachment. The unedited program reproduces the raw Kiln GLB (before Rupa3D
        metadata) with the same Kiln version and runtime identity recorded in the provenance.
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: string }
      responses:
        '200':
          description: Program source
          content:
            text/plain: { schema: { type: string } }
        '404': { description: Not a successful teks_ke_3d job of this customer }
  /v1/job/{id}/lembar.png:
    get:
      summary: Download the Kiln contact sheet of a successful teks_ke_3d job
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: string }
      responses:
        '200':
          description: PNG contact sheet
          content:
            image/png: { schema: { type: string, contentEncoding: binary } }
        '404': { description: Not a successful teks_ke_3d job of this customer }
  /v1/saldo:
    get:
      summary: Meter balance minus active local reservations
      responses:
        '200': { description: meter, held, and available credits }
  /mcp:
    post:
      summary: MCP Streamable HTTP endpoint
      description: >-
        Exposes rupa_aset_siap_fisika and rupa_gambar_ke_3d with the same bearer authentication and credit ledger.
        rupa_teks_ke_3d and rupa_job are listed only while teks_ke_3d is enabled and ready.
        Batches contain at most 100 JSON-RPC messages; larger batches are rejected before tool execution.
      responses:
        '200': { description: MCP JSON or streamable response }
        '202': { description: Notification batch accepted without response body }
        '400': { description: Invalid JSON-RPC request, including batch over 100 messages (code -32600) }
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
  responses:
    Unauthorized:
      description: Missing or invalid license/API key