Guide

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.

publish.ts
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 paid
9 frameworks: ["pytorch", "ros2"],
10});
11
12console.log(`https://www.nodedata.ai/marketplace/${asset.slug}`);

Field notes

FieldNotes
titleRequired, 1–140 characters. The slug is derived from it and de-duplicated automatically — you don't choose it.
descriptionRequired, up to 8,000 characters. Buyers read this first; lead with what the artifact is and what it was trained on.
typeRequired. model, dataset, policy, workflow, and similar.
price_centsInteger cents. 0 is free; paid listings start at 100 ($1.00). Anything between 1 and 99 is rejected with price_too_low.
licenseDefaults to MIT. Pick deliberately — it governs what buyers may do with the artifact.
versionDefaults 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.

upload-then-publish.ts
1// 1. Ask for an upload target
2const { 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 there
10await fetch(url, {
11 method: "PUT",
12 body: file,
13 headers: { "content-type": "application/zip" },
14});
15
16// 3. Publish, referencing the object
17const 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

It's part of the app API rather than the versioned public surface, which is why the SDK reaches it through the request() escape hatch. Check the upload guide for the current path.

Editing and unpublishing

maintain.ts
// 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.

earnings.ts
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.