Publishing listings
POST /models creates and publishes a listing, held to the same validation rules as the web upload flow. It needs the models:upload scope.
Metadata only
A listing without an artifact is valid — useful for a hosted API, a pointer to external weights, or a placeholder you'll attach files to later.
1const asset = await nd.models.create({2 title: "Bin-Picking Policy v2",3 description: "Diffusion policy trained on 40k bimanual grasps. ROS 2 node included.",4 type: "policy",5 category: "manipulation",6 license: "Apache-2.0",7 version: "2.0.0",8 price_cents: 4900, // integer cents; 0 for free, min 100 for paid9 frameworks: ["pytorch", "ros2"],10});11 12console.log(`https://www.nodedata.ai/marketplace/${asset.slug}`);Field notes
| Field | Notes |
|---|---|
title | Required, 1–140 characters. The slug is derived from it and de-duplicated automatically — you don't choose it. |
description | Required, up to 8,000 characters. Buyers read this first; lead with what the artifact is and what it was trained on. |
type | Required. model, dataset, policy, workflow, and similar. |
price_cents | Integer cents. 0 is free; paid listings start at 100 ($1.00). Anything between 1 and 99 is rejected with price_too_low. |
license | Defaults to MIT. Pick deliberately — it governs what buyers may do with the artifact. |
version | Defaults to 1.0.0. Bump it whenever you replace the artifact, not just the metadata. |
Attaching a file
Uploads happen out of band: you get an upload URL, put the bytes there, then reference the resulting storage path when creating the listing. The path must live under your own user/{id}/ prefix and the object must already exist — otherwise you get invalid_storage_path or file_not_found.
1// 1. Ask for an upload target2const { url, storage_path } = await nd.request<{3 url: string;4 storage_path: string;5}>("POST", "/../assets/upload-url", {6 body: { file_name: "policy.zip", content_type: "application/zip" },7});8 9// 2. Put the bytes there10await fetch(url, {11 method: "PUT",12 body: file,13 headers: { "content-type": "application/zip" },14});15 16// 3. Publish, referencing the object17const asset = await nd.models.create({18 title: "Bin-Picking Policy v2",19 description: "…",20 type: "policy",21 price_cents: 4900,22 storage_path,23 file_name: "policy.zip",24 file_size: file.size,25 file_extension: "zip",26});The upload-url endpoint lives outside /v1
request() escape hatch. Check the upload guide for the current path.Editing and unpublishing
// Reprice or bump metadata (needs listings:write)await nd.models.update("bin-picking-policy-v2", { price_cents: 3900, version: "2.0.1",}); // Take it off the marketplace. Buyers keep what they paid for.await nd.models.del("bin-picking-policy-v2");Ownership is enforced on both: another account's listing reads as a 404 rather than a 403, so the API can't be used to probe for what exists.
Getting paid
Buyers pay the platform through Checkout. Each completed sale records a 25/75 split — 25% platform fee, 75% to you — and your share accrues until a payout account is connected.
const payouts = await nd.payouts.retrieve({ limit: 50 }); console.log(payouts.summary.net_cents / 100, "USD earned"); // Distinguishes "nothing owed" from "owed but not moving"if (payouts.payouts_paused) { console.log("Payouts are paused platform-wide; balances keep accruing.");}See pricing & revenue for the authoritative terms, and webhooks to react to listing.purchased in real time instead of polling.