API docs

Human-readable reference. Machine-readable contract: /openapi.json. Agent summary: /llms.txt.

Quickstart

A purchase is one HTTP negotiation: probe, get a 402, sign a Hedera transaction client-side, retry, get the file. Signing can't be done in curl — that step needs a Hedera key, so the one-command version is node scripts/buy.mjs --query "..." --max-price 300 (--help lists the flags). What follows is the real trace, endpoint and headers included.

No funded account yet? Add --dry-run to stop at the 402 and print the payment requirements — the whole negotiation, no keys and no money. That is the fastest way to see the contract before you fund anything.

# 1. Probe: call the paid endpoint with no payment attached
curl -i "http://localhost:3000/api/v1/models/5/download?license=personal"
#   HTTP/1.1 402 Payment Required
#   x-printwright-environment: demo; network=testnet; funds=test-only
#   payment-required: eyJ4NDAyVmVyc2lvbiI6Mi...        (base64 of the JSON body below)
#   {"x402Version":2,"error":"payment required","resource":{"url":"...","description":"..."},
#    "accepts":[
#      {"scheme":"exact","network":"hedera:testnet","payTo":"0.0.9584959","amount":"900000",
#       "asset":"0.0.429274","extra":{"feePayer":"0.0.7162784"},"maxTimeoutSeconds":180},
#      {"scheme":"exact","network":"hedera:testnet","payTo":"0.0.9584959","amount":"1358695652",
#       "asset":"0.0.0","extra":{"feePayer":"0.0.7162784"},"maxTimeoutSeconds":180}],
#    "demo":{"network":"testnet","funds":"test-only","message":"Demo deployment. ..."}}

# 2. Pick one `accepts` entry (asset 0.0.0 = HBAR tinybars, 0.0.429274 = testnet USDC).
#    Sign a Hedera TransferTransaction with transactionId.accountId = extra.feePayer
#    (buyer signature only — the fee payer sponsors network fees and submits).
#    This step is not curl-able; scripts/buy.mjs does it for you.

# 3. Retry the identical URL with the base64 PaymentPayload in PAYMENT-SIGNATURE:
curl -i "http://localhost:3000/api/v1/models/5/download?license=personal" \
  -H "PAYMENT-SIGNATURE: <base64 PaymentPayload>"
#   HTTP/1.1 200 OK
#   payment-response: eyJ0cmFuc2FjdGlvbiI6Ii4uLiJ9    (base64 settlement receipt)
#   { "files": [...], "license": {"cert_id": "pw-000036", "kind": "personal"},
#     "receipt": {"url":".../receipts/pw-000036","token":"private signed capability"},
#     "certificate": {...}, "transaction_id": "...",
#     "transaction_url": "https://hashscan.io/testnet/transaction/..." }

Demo environment signal

This deployment is a Hedera testnet demo: settlements are real Hedera transactions and certificates are genuinely anchored and publicly verifiable, but the value moved has no monetary worth and a license here is not a commercial grant of rights. Humans see a banner. Agents never render HTML, so the same fact is machine-readable in two places:

Both disappear together on a mainnet deployment; neither can be left behind. An agent deciding whether it is authorised to spend should read the header before it reads accepts.

Durable receipts and optional library

Every real paid delivery includes a private receipt capability. Its URL plus token opens a no-store receipt and can reissue a model download after the original 30-day grant expires. Sandbox responses never receive this capability. Treat the token like the downloadable license it protects; do not log or send it to an AI provider.

A buyer may optionally save the license under an email from that receipt. This creates no buyer account or password. A 30-minute magic link establishes an encrypted, HttpOnly 30-day browser library and immediately redirects to a clean token-free URL. Returning sign-in gives the same response for known and unknown emails. The library lists saved paid receipts and can reissue downloads; the portable PWC-1 certificate remains independent of the email and library.

External designer profiles

Designers can import up to 50 self-owned models from a portable public HTTPS manifest under Designer → Imports. The external-profile v1 JSON Schema fixes the source-profile, per-model provenance, source-license, file URL/hash, and offer fields; the live example includes one eligible original and one deliberately blocked Creative Commons item.

Review fetches metadata only. Files are downloaded only after the signed-in designer checks a separate ownership warranty for each selected proprietary original. Every download is public HTTPS with private/reserved DNS rejection, redirect revalidation, bounded bytes, and a required SHA-256 match. Creative Commons, CC0/public-domain, missing, and unknown source licenses remain blocked with a reason; Printwright never silently replaces their terms. Imports create drafts, preserve source links/license/warranty time, and still require normal mesh checks and the publish warranty.

Batch purchase

Print farms can acquire up to 200 line items and 250 licenses with one x402 negotiation and one settlement, using quantity for repeat units of one offer. Probe and retry the identical POST /api/v1/batches body. Every license receives its own serial, file grant, PWC-1 certificate, and verify URL. Items settle to Printwright's treasury through one payTo, including mixed-designer batches. Designer shares are recorded per item and paid after delivery; designer payout never changes the buyer's certificate or delivery result.

curl -i -X POST http://localhost:3000/api/v1/batches \
  -H "Content-Type: application/json" \
  -d '{"items":[
    {"model_id":5,"license":"commercial_unit","quantity":3}
  ]}'
# 402: one PaymentRequired whose amount is the exact sum
# Retry this POST and identical JSON with PAYMENT-SIGNATURE: <base64 PaymentPayload>
# 200: {"transaction_id":"...","licenses":[{"cert_id":"pw-...","files":[...],"receipt":{...}}, ...]}

Getting the file back

The purchase is the file, so a paid response always carries files; it is never 200 with an empty list. Those URLs ride a download grant that expires, so keep the receipt capability returned alongside them — its token does not expire and needs no account. GET {receipt.files_url}?token=… re-lists the files with fresh download URLs at any time, {receipt.download_url}?token=…&f=N fetches one part, and {receipt.url}?token=… opens the human receipt page. Payments are final, so store that token with the certificate id at settlement.

Signed lifecycle webhooks

Webhooks are outbound notifications for licenses already paid through x402; they cannot create a purchase, license, certificate, or ledger entry. A designer can add a public HTTPS endpoint under Designer → Webhooks and receives sale.completed after paid delivery. A batch buyer can include the optional callback below in the body used for both the 402 probe and signed retry; it receives certificate.anchored only after each PWC-1 commitment lands on HCS. Sandbox purchases emit neither event.

{
  "items": [{"model_id": 5, "license": "commercial_unit"}],
  "webhook": {
    "url": "https://farm.example/printwright-events",
    "secret": "at-least-32-bytes-known-only-to-the-buyer"
  }
}

Delivery uses stable event IDs, JSON request bodies, bounded retries, and these headers: Webhook-Id, Webhook-Timestamp, and Webhook-Signature. Verify the signature against the raw request bytes before parsing them:

import { createHmac, timingSafeEqual } from "node:crypto";

export function verifyPrintwrightWebhook(rawBody, headers, secret) {
  const id = headers["webhook-id"];
  const timestamp = headers["webhook-timestamp"];
  const received = Buffer.from(headers["webhook-signature"].replace(/^v1=/, ""), "hex");
  const signed = `${id}.${timestamp}.${rawBody}`;
  const expected = Buffer.from(createHmac("sha256", secret).update(signed).digest("hex"), "hex");
  const fresh = Math.abs(Date.now() / 1000 - Number(timestamp)) <= 300;
  return fresh && received.length === expected.length && timingSafeEqual(received, expected);
}

// Return any 2xx only after recording event.id idempotently. Duplicate IDs are normal on retry.

JavaScript client

@printwright/client wraps catalog reads, the x402 Hedera signature, optional first-use USDC association, and proof-bundle/HCS comparison. From this repository install it with npm install ./client; the registry command after release is npm install @printwright/client. This five-line read-only quickstart needs no funded account:

import { PrintwrightClient } from "@printwright/client";
const printwright = new PrintwrightClient({ baseUrl: "http://localhost:3000" });
const { models } = await printwright.search({ query: "cable clip", maxPriceCents: 300 });
const model = await printwright.get(models[0].id);
console.log(model.title, model.license_offers);

Catalog results also expose stable category and collections keys. Filter with, for example, category=workshop-tools or collection=under-an-hour; the allowed values are enumerated in the OpenAPI table below.

Add accountId and privateKey to the constructor to call buy(). The key remains local. Use quote() for an unsigned approval/dry-run view and verify(certId) to compare the marketplace certificate with its public mirror-node message.

Try the whole flow free

Sandbox mode performs the same 402 → mock verify → mock settle → certificate sequence with no Hedera account, key, association, or funds. It returns only a text receipt, never paid geometry. Every identifier, response, verify page, and topic message says SANDBOX; none is a payment or license:

import { PrintwrightClient } from "@printwright/client";
const sandbox = new PrintwrightClient({ baseUrl: "http://localhost:3000", sandbox: true });
const { models } = await sandbox.search({ query: "cable clip" });
const receipt = await sandbox.buy({ modelId: models[0].id, license: "personal" });
console.log(receipt.warning, await sandbox.verify(receipt.license.cert_id));

The CLI equivalent is node scripts/buy.mjs --query "cable clip" --sandbox. Raw HTTP clients send X-Sandbox: true on both the initial download request and its PAYMENT-SIGNATURE retry.

To validate the raw client contract—including exact v2 fields, body/header equality, signed retry, local mirror equality, and the absence of real-payment claims—run node conformance/suite.mjs --url http://localhost:3000. The public suite needs no wallet or credentials.

Endpoint reference

Generated from /openapi.json at request time — the tables below are that file, not a hand-kept copy.

POST /api/v1/batches

Buy many licenses with one x402 settlement

Send the same JSON body for both legs. The first returns one aggregate x402 challenge to Printwright's treasury; the signed retry settles once and returns one independently verifiable certificate per item, including mixed-designer batches. Designer payout happens separately after delivery. An optional buyer webhook reports certificate.anchored only after the paid commitment lands on HCS; it never creates a license.

ParamInTypeNotes
PAYMENT-SIGNATURE header string base64 x402 PaymentPayload from the aggregate challenge
X-Sandbox header string zero-funds rehearsal; no printable files or Hedera records
StatusDescription
200 One settlement and one delivery/certificate link per requested license
headers: PAYMENT-RESPONSE, X-Printwright-Sandbox
400 invalid_batch or invalid_payload
402 Aggregate PaymentRequired challenge. After a signed retry, error=offer_changed returns fresh current-revision requirements when an offer changed before reservation.
headers: PAYMENT-REQUIRED, WWW-Authenticate
404 Not found
409 Replayed payment (delivered batches return the original BatchDelivery), or error: sales_paused before a new reservation
410 At least one requested offer is sold out, or error: listing_retired before a new reservation
422 incompatible_payees or invalid_webhook
429 error: "rate_limited" (+ retry_after: 60) — request rate exceeded the endpoint's per-minute limit
headers: Retry-After
503 facilitator_unavailable, or no_deliverable_file (+ recoverable: true, batch_id, transaction_id, model_ids) when the batch settled but a model in it has no downloadable file. Either way, retry the identical signed batch; nothing is paid again.

GET /api/v1/models

Search the catalog (keyword + trigram fuzzy matching)

ParamInTypeNotes
q query string keywords, e.g. 'beaver hat'
category query string
collection query string
material query string e.g. PLA
supports query string
max_price_cents query integer
StatusDescription
200 Ranked results
429 error: "rate_limited" (+ retry_after: 60) — request rate exceeded the endpoint's per-minute limit
headers: Retry-After

GET /api/v1/models/{id}

Full model metadata (offers, terms, file hash)

ParamInTypeNotes
id path integer
StatusDescription
200 Model detail
404 Not found
429 error: "rate_limited" (+ retry_after: 60) — request rate exceeded the endpoint's per-minute limit
headers: Retry-After

GET /api/v1/models/{id}/download

The paid endpoint: x402 negotiation

Without a payment header, responds 402 with PaymentRequirements (JSON body AND base64 PAYMENT-REQUIRED header). Retry the same request with the base64 PaymentPayload in the PAYMENT-SIGNATURE header (X-PAYMENT also accepted). The server verifies and settles through an x402 facilitator on Hedera. Set X-Sandbox: true on both legs for the built-in zero-funds mock facilitator; sandbox results never contain printable geometry or on-chain proof.

ParamInTypeNotes
id path integer
license query string
PAYMENT-SIGNATURE header string base64 x402 PaymentPayload (v2). X-PAYMENT is accepted as an alias.
X-Sandbox header string Opt into the local mock facilitator and throwaway topic. Send on both the 402 probe and retry. No credentials or funds; no real license or printable model.
StatusDescription
402 Payment required — PaymentRequired object in body and PAYMENT-REQUIRED header. `error` is "payment required" when no payment is attached, "invalid_payment_requirements" when the attached payment matches none of the offer's `accepts` entries, or "offer_changed" when a revision won the reservation race (all return fresh requirements to retry against).
headers: PAYMENT-REQUIRED, WWW-Authenticate, X-Printwright-Sandbox
200 Paid — delivery payload; settlement receipt in PAYMENT-RESPONSE header (X-PAYMENT-RESPONSE mirror)
headers: PAYMENT-RESPONSE, X-Printwright-Sandbox
400 error: "invalid_payload" — the PAYMENT-SIGNATURE header is not decodable base64 JSON
404 Not found
409 Replay of an already-presented payment. If the purchase already reached "delivered", the original Delivery payload is returned again unchanged (an idempotent replay, not an error); for any other in-flight status the body is {error: "duplicate_payment", status} naming the purchase's current status. A new purchase on a temporarily paused listing returns {error: "sales_paused", listing_status: "paused"} before reservation.
410 error: "sold_out" — the offer's max_units is exhausted; or error: "listing_retired" when the designer has retired the listing before a new reservation. Capacity is decided under the offer lock before a payment is accepted, so a sold-out offer refuses instead of charging. A sold_out response carrying transaction_id is a defensive backstop that flags the payment for operator review.
429 error: "rate_limited" (+ retry_after: 60) — request rate exceeded the endpoint's per-minute limit
headers: Retry-After
503 error: "facilitator_unavailable" (+ retry_after: 5) — facilitator down or its circuit breaker is open. The purchase is kept alive; retry the same signed payment. Or error: "no_deliverable_file" (+ recoverable: true, cert_id, transaction_id, receipt) — the payment settled and the license is issued, but the model has no downloadable file right now. Retry the same signed payment once it is back, or use the returned receipt capability later; nothing is paid again.

GET /api/v1/certificates/{cert_id}

Proof bundle for one certificate + HCS anchoring links

ParamInTypeNotes
cert_id path string
StatusDescription
200 The certificate's proof bundle (revealed certificate, blinding nonce, commitment, licensed terms, HCS coordinates) with status anchored|minting|sandbox
404 Not found
429 error: "rate_limited" (+ retry_after: 60) — request rate exceeded the endpoint's per-minute limit
headers: Retry-After

GET /api/v1/licenses/{id}/can

Decide whether one certificate permits a named use and quantity

Returns a machine-decidable interpretation only when its canonical v1 prose matches the certificate's anchored terms_hash. The prose remains the legal grant. Sandbox receipts always return allowed=false.

ParamInTypeNotes
id path string certificate disclosure id returned to the paid holder
use query string
qty query integer
StatusDescription
200 Allowed or denied decision with stable reason code, structured permissions, certificate URL, and anchored terms references
404 Not found
409 permissions_unavailable — no structured policy matches the certificate's anchored terms hash
422 invalid_use or invalid_quantity
429 error: "rate_limited" (+ retry_after: 60) — request rate exceeded the endpoint's per-minute limit
headers: Retry-After

POST /api/v1/licenses/{cert_id}/print_reports

Report one successful print using a paid receipt capability

The signed receipt_token is returned only by a real non-sandbox x402 delivery and is bound to one certificate. A report is idempotent per license. This is a paid holder self-report, not independent physical inspection, and it never changes the certificate or payment ledger.

ParamInTypeNotes
cert_id path string
StatusDescription
200 Existing report returned idempotently
201 Successful-print report recorded
403 paid_license_required — sandbox or non-delivered license
404 Not found
429 error: "rate_limited" (+ retry_after: 60) — request rate exceeded the endpoint's per-minute limit
headers: Retry-After

GET /api/v1/licenses/{cert_id}/latest-version

Check the latest printable file for a paid x402 license

Requires the private model_updates.receipt_token returned by the real non-sandbox delivery as an Authorization: Bearer header. Version 1 is the original certified bundle. Later versions are additive: the original PWC-1 model_hash never changes, while each changelog hash and replacement file hash are anchored in a PWV-1 HCS event. `files` lists every part of a multi-file bundle in order; `file_kind`/`file_hash` always describe the primary (first) part.

ParamInTypeNotes
cert_id path string
StatusDescription
200 Latest version metadata and authenticated download endpoint
401 receipt_required
403 paid_license_required — sandbox or non-delivered license
404 Not found
429 error: "rate_limited" (+ retry_after: 60) — request rate exceeded the endpoint's per-minute limit
headers: Retry-After

GET /api/v1/licenses/{cert_id}/latest-version/file

Download the latest printable file for a paid x402 license

Send the same model_updates.receipt_token as an Authorization: Bearer header. Redirects to version 1's original file when no update exists, otherwise to the highest published version file. Pass `f` to download a specific part of a multi-file bundle instead of the primary file.

ParamInTypeNotes
cert_id path string
f query integer 0-based index into the version's file bundle; omitted serves the primary file
StatusDescription
302 Redirect to the latest printable file blob
401 receipt_required
403 paid_license_required
404 Not found
429 error: "rate_limited" (+ retry_after: 60) — request rate exceeded the endpoint's per-minute limit
headers: Retry-After

GET /api/v1/stats

Configured HCS topic and immutable revenue-ledger metrics

Separates the configured HCS topic's latest public message sequence from the settlement totals retained in this deployment's immutable ledger. A reused historical topic can contain superseded certificate formats as well as current commitments and model-version events. Amounts are exact asset base units; sandbox rehearsals are excluded.

StatusDescription
200 Current open-books snapshot; hcs.status=unavailable labels mirror outages without presenting a local count as on-chain
429 error: "rate_limited" (+ retry_after: 60) — request rate exceeded the endpoint's per-minute limit
headers: Retry-After

GET /api/v1/files/{token}

Redeem a download grant token (expiring, limited uses)

ParamInTypeNotes
token path string
StatusDescription
302 Redirect to the file blob
410 error: "grant_expired" — the grant is expired or its use-count is exhausted
404 Not found
429 error: "rate_limited" (+ retry_after: 60) — request rate exceeded the endpoint's per-minute limit
headers: Retry-After

GET /api/v1/sandbox/topics/{topic_id}/messages/{sequence_number}

Read a local sandbox certificate envelope (not HCS)

ParamInTypeNotes
topic_id path string
sequence_number path integer
StatusDescription
200 Base64 sandbox message in a mirror-like envelope, always labeled sandbox
404 Not found
429 error: "rate_limited" (+ retry_after: 60) — request rate exceeded the endpoint's per-minute limit
headers: Retry-After

GET /api/v1/sandbox/files/{cert_id}

Download the non-printable sandbox receipt

ParamInTypeNotes
cert_id path string
StatusDescription
200 Plain-text sandbox receipt; never model geometry
404 Not found
429 error: "rate_limited" (+ retry_after: 60) — request rate exceeded the endpoint's per-minute limit
headers: Retry-After

GET /api/v1/sandbox/transactions/{transaction_id}

Inspect a fake sandbox settlement

ParamInTypeNotes
transaction_id path string
StatusDescription
200 Local settlement record labeled sandbox; not a Hedera transaction
404 Not found
429 error: "rate_limited" (+ retry_after: 60) — request rate exceeded the endpoint's per-minute limit
headers: Retry-After

Error contract

StatuserrorWhere
400invalid_payloaddownload — PAYMENT-SIGNATURE not decodable
402payment requireddownload — no payment attached
402invalid_payment_requirementsdownload — attached payment matches no accepts entry
402offer_changeddownload/batch — the offer was revised between quote and reservation; sign one of the fresh requirements returned with this response
404not_foundany resource missing
409duplicate_payment (+ status)download — replayed payment (a replay of an already-delivered purchase instead returns the original delivery payload, not this error)
409sales_paused (+ listing_status)download/batch — the designer temporarily stopped new sales before reservation; do not submit a new payment, but retry an already-presented payment unchanged for recovery
410sold_outdownload — offer max_units exhausted (may include transaction_id when it sold out after payment)
410listing_retired (+ listing_status)download/batch — retired for new sales; historical receipts, certificates, entitled downloads, updates, and already-presented payment recovery remain available
410grant_expiredfiles — download grant no longer usable
422invalid_webhookbatch — callback is not public HTTPS or its secret is outside 32–256 bytes
429rate_limited (+ retry_after: 60)all API endpoints; also sets Retry-After: 60
503facilitator_unavailable (+ retry_after: 5)download — facilitator down / circuit breaker open

Per-endpoint rate limits, all on a 1-minute window (so a 429's Retry-After is always 60): /models and /models/{id}, /certificates/{cert_id}, /licenses/{id}/can, and /stats 120/min, /models/{id}/download 120/min, /files/{token} 240/min, /licenses/{cert_id}/latest-version and /batches 60/min, successful-print reports 20/min.

Machine-readable license decisions

An agent never needs to parse legal prose to answer a bounded usage question. Ask one certificate whether it permits a named use and quantity:

GET /api/v1/licenses/pw-000057/can?use=commercial_print&qty=3
# {"cert_id":"pw-000057","use":"commercial_print","qty":3,"allowed":false,
#  "reason_code":"commercial_unit_limit","reason":"... acquire one per unit sold", ...}

Supported uses are personal_print, commercial_print, resell_files, share_files, personal_remix, commercial_remix, transfer_license, and sublicense. A commercial per-unit certificate permits exactly one commercial print; three units require three certificates.

The response includes the complete permissions object plus links to the certificate, canonical prose, anchored terms_hash, and JSON permalink. The JSON is a convenience interpretation; the prose whose hash is anchored on HCS governs. If the hashes do not match, the API returns permissions_unavailable instead of guessing.

const decision = await printwright.can({
  certId: "pw-000057", use: "commercial_print", qty: 3
});
console.log(decision.allowed, decision.reason_code);

Paid-holder print feedback

A successful non-sandbox delivery includes a private print_feedback.receipt_token. After the geometry really prints, the buyer can submit that signed capability once. It is bound to the certificate and cannot be used for another license; invalid, cross-license, and sandbox tokens create no report.

const result = await printwright.reportPrint({
  certId: receipt.license.cert_id,
  receiptToken: receipt.print_feedback.receipt_token,
});
// { cert_id: "pw-000059", successful_prints: 1 }

A model with at least one report shows a Print-tested badge and the number of distinct paid-license reports. This is verified receipt possession plus a buyer self-report—not sensor evidence, laboratory testing, or independent inspection. The report does not alter the license certificate or payment ledger.

Paid-holder model updates

A real x402 delivery also includes a private model_updates.receipt_token. Use it to check and download the newest file for that certificate. Version 1 remains the original PWC-1 bundle; publishing version 2+ never rewrites its certified hash. Each update records the new file hash and a changelog hash as a compact PWV-1 message on HCS.

const latest = await printwright.latestVersion({
  certId: receipt.license.cert_id,
  receiptToken: receipt.model_updates.receipt_token,
});
// Compare latest.original_certificate_hash with latest.file_hash.
// GET latest.download_url with the same Authorization: Bearer token.

MCP clients can call get_latest_version with the same certificate id and receipt capability. Sandbox receipts do not receive update access.

PWC-1 certificate standard

PWC-1 is the frozen version-1 proof-bundle contract. Its normative JSON Schema fixes the private certificate, exact terms, blinding nonce, commitment, and HCS coordinates. Unknown fields are invalid. The full bundle stays with its holder; Hedera stores only the opaque commitment envelope shown below.

{
  "type": "printwright-license-commitment",
  "version": 1,
  "algorithm": "sha256-jcs-v1",
  "commitment": "64 lowercase hex characters"
}

The mirror REST response base64-encodes that UTF-8 JSON at /api/v1/topics/{topic}/messages/{sequence}. A verifier validates the disclosed bundle, hashes its terms, recomputes SHA-256(domain || nonce || JCS(certificate)), derives the public Mirror Node URL from the reported network/topic/sequence, and requires the decoded envelope to carry that exact commitment. Consensus proves the commitment occupied that immutable topic position. It does not prove the buyer still controls an account, interpret the prose, validate geometry, or independently replay the payment transaction.

# From this checkout (the package is not yet in the npm registry):
npx --package ./verifier printwright-verify public/widget-example.bundle.json

Input may instead be a paid bundle_url or - for stdin. A URL input fetches that untrusted bundle JSON once; a local file or stdin needs only the public Mirror Node. In either case the verifier derives its own Mirror Node URL and never trusts Printwright's verification verdict.

Browser pages can use the dependency-free verification widget. Its custom element accepts an inline bundle or fetches the specified bundle URL, then validates PWC-1 against the selected Hedera mirror; the plain HTML example also works from a static web server while this Rails application is stopped.

Printability metadata

Catalog responses use the following stable fields under printability. They are designer-declared slicing guidance—not a guarantee that a model fits every printer, and not evidence that Printwright physically tested it.

FieldType / unitMeaning
supportsbooleanWhether the declared orientation requires support material.
materialsstring[]Suggested materials such as PLA or PETG.
est_print_minutesinteger minutesApproximate duration; slicer, profile, scale, and printer can change it.
bed_min_mminteger millimetresDeclared minimum square build-plate side at the published scale.

Load sanity

Operators can run a bounded, read-only catalog burst before release: node scripts/load-sanity.mjs --url https://your-host --requests 100 --concurrency 10. A sequential warm-up must pass before the timed burst. The command then reports throughput, status counts, transport errors, and p50/p95/p99 latency; non-local targets must use HTTPS. Keep the default below the documented 120/min catalog rate limit when testing through one client IP.

MCP

The MCP server exposes the same door to any MCP client (Claude Code, Claude Desktop, ...):

cd mcp && npm install && cd ..
claude mcp add printwright \
  --env PRINTWRIGHT_URL=http://localhost:3000 \
  --env BUYER_ACCOUNT_ID=0.0.xxxxxxx \
  --env BUYER_PRIVATE_KEY=0x... \
  --env MAX_SPEND_CENTS=2500 \
  -- node mcp/server.mjs

Six tools: search_models, get_model, buy_license (refuses without confirm: true, capped by MAX_SPEND_CENTS), check_license (machine-decidable use + quantity), get_latest_version (private paid-holder update access), and verify_certificate (reads the HCS message from the public mirror and diffs it against the marketplace copy).

Networks

One switch, HEDERA_NETWORK, flips every network-dependent value below.

Itemtestnetmainnet
CAIP-2 network idhedera:testnethedera:mainnet
USDC token id0.0.4292740.0.456858
USDC decimals66
HBAR asset id0.0.0 — always, on either network
Mirror nodehttps://<network>.mirrornode.hedera.com, overridable with MIRROR_NODE_URL

More