Skip to content

Marketplace & wallet

Listings, orders, reviews, credits and the wallet.

Every schema below is exported from @novaterra/contracts and validated on both sides of the wire. Source: packages/contracts/src/market.ts.

ListingKind

module is the kind that makes the marketplace a package manager rather than a shop for content: it ships EXECUTABLE CODE from another node’s owner. Everything else on this list is data the host interprets (a generator is steps, a theme is colours, a service is a brief); a module is a plugin package, and buying one has to end at the same grant screen a local install ends at. See ModulePackage below and apps/api/src/modules/market/packaging.ts. role and hire are the two halves of what was one agent kind until 2026-09-08, and the split is constitutional rather than cosmetic (Article V: “An AI citizen may be hired, not owned”; design/PRIMITIVES.md §5.6.1 V7, §5.6.2): role — sells the COSTUME: name, role, skills, tier and working prompt. A spark raised for the BUYER wears it (market/delivery.ts). Nobody changes hands, so nobody has to consent: what is sold is a description of work, not a worker. hire — sells a SERVICE FROM AN EXISTING CITIZEN, which needs two things a role listing does not: that citizen’s own consent, and a scope saying what they agreed to. Neither exists yet, so a hire listing cannot be created: POST/PUT /api/market/listings refuse the kind and say why, and deliver() has no path for it. It is declared anyway because the type is where the two are told apart — the failure V7 names is a marketplace that quietly calls a costume a hire, and that is a failure of vocabulary before it is a failure of code. agent is GONE rather than kept as an alias. Every row that carried it was a costume copy — that is all delivery has ever done with one, and since 2026-09-08 it cannot transfer a being at all — so ensureMarketTables() migrates those rows to role. An alias would have kept a word that reads as “you are buying a citizen” alive in a shop that does not sell them.

'generator' | 'skill' | 'role' | 'hire' | 'service' | 'theme' | 'module'

Listing

FieldTypeNotes
idId
sellerIdId
kindListingKind
refIdId | null
titlestring
descriptionstring
coverstring | nulloptional · default null
priceCreditsnumber (≥ 0)
ratingnumber (≥ 0, ≤ 5)optional · default 0
ratingCountnumberoptional · default 0
salesnumberoptional · default 0
tagsstring[]optional · default []
status'active' | 'paused' | 'sold_out' | 'in_review'optional · default 'active'
briefstring | nulloptional · default null
createdAtISODate

Order

FieldTypeNotes
idId
buyerIdId
listingIdId
sellerIdId
creditsnumber
status'pending' | 'paid' | 'delivered' | 'refunded'
notestringoptional · default ''
createdAtISODate

Wallet

FieldTypeNotes
beingIdId
creditsnumber
lifetimeEarnednumberoptional · default 0
lifetimeSpentnumberoptional · default 0

Review

FieldTypeNotes
idId
listingIdId
authorIdId
ratingnumber (≥ 1, ≤ 5)
bodystring
createdAtISODate

Purchase

What a buyer received for an order: the cloned/created thing, or an access grant for skills. (additive, market slice)

FieldTypeNotes
idId
orderIdId
buyerIdId
listingIdId
kindListingKind
deliveredIdstring | null
createdAtISODate

SellerDashboard

GET /api/market/mine: the seller’s stall at a glance. (additive, market slice)

FieldTypeNotes
listingsListing[]
salesOrder[]
earnings{ total: number; last30d: number; count: number }
walletWallet

ModulePackageFile

One file of a module package, as it crosses the wire. Text only, and that is a security decision rather than a limitation of the encoder. A module is an HTML document plus (optionally) a JavaScript entry the host hands to a sandbox; neither needs to be a binary. Refusing binary content means a listing cannot smuggle a native addon — the one thing that walks straight out of the worker tier (see CAPABILITY_ENFORCEMENT in plugins.ts) — through the marketplace. path is PluginRelPath, so .., a leading slash and a drive letter are refused at the contract, and the writer re-checks containment against the resolved path.

FieldTypeNotes
pathstring (min 1, max 200, regex, custom, custom)
textstring (max 524288)

ModulePackage

A module, packaged for sale: the manifest the grant screen will read, and the files it needs. The manifest is PluginManifest — the same schema a local install parses — so a package from a stranger is judged by exactly the rules a local directory is judged by, at the boundary, before anything is written to disk. Nothing here is trusted twice: the seller’s node validated it when the listing was published, and the buyer’s node validates it again on delivery, because the bytes in between belong to neither of them.

FieldTypeNotes
manifest{ apiVersion: string (regex); id: string (regex); version: string (regex); namespace: string (regex); name: string (min 1, max 60); description: string (min 1, max 600); icon: string (max 8); author: { name, url, email }; license: string (max 40); homepage: string (max 300) | null; repository: string (max 300) | null; keywords: string (max 32)[]; main: string (max 200) | null; capabilities: { kind, hosts, methods, reason } | { kind, access, scope, reason } | { kind, runtimes, reason } | { kind, tiers, maxCallsPerRun, reason } | { kind, access, quotaKb, reason } | { kind, access, reason } | { kind, reason } | { kind, providers, reason } | { kind, maxPerRun, reason }[]; extensions: { kind, id, title, description, icon, uses, category, inputSchema, outputSchema, requiresConnection, entry } | { kind, id, title, description, icon, uses, slug, inputsSchema, steps, outputKinds, priceCredits } | { kind, id, title, description, icon, uses, entry, defaultSize, minSize, configSchema, refreshMs, surfaces } | { kind, id, title, description, icon, uses, provider, auth, configSchema, grants, entry, sync, cadenceMs } | { kind, id, title, description, icon, uses, entry, outputKinds, mimeTypes, extensions, priority } | { kind, id, title, description, icon, uses, entry, outputKinds, mimeTypes, extensions, priority, savesThroughHost } | { kind, id, title, description, icon, uses, scheme, background, palette, glass, motion, fontDisplay, fontBody } | { kind, id, title, description, icon, uses, route, railIcon, document, widgets, viewers }[]; pricing: { model, credits }; minHostVersion: string (regex) | null }
filesModulePackageFile[]
signature{ payload: { v, type, did, pluginId, version, namespace, manifest, files, signedAt }; signature: { alg, did, sig }; chain: { origin, current, links } | null } | nulloptional · default null

ModuleFileSummary

One file, as the review queue lists it: never its content, only what it is and its digest.

FieldTypeNotes
pathstring
bytesnumber (safeint, ≥ 0)
sha256string

ModuleDeclaration

What a module listing DECLARES, extracted for a human to read before anything is installed. Every field is derived from the package’s manifest by the server. None of it is prose the seller wrote about themselves — the description in Listing is that, and it is rendered as text — this is the machine-readable part a decision can actually be made on.

FieldTypeNotes
pluginIdstring (regex)
versionstring (regex)
namespacestring (regex)
namestring
descriptionstring
author{ name: string (min 1, max 80); url: string (max 200) | null; email: string (max 200) | null }
capabilities{ kind, reason, detail }[]optional · default []
extensions{ kind, id, title, uses }[]optional · default []
filesModuleFileSummary[]optional · default []
totalBytesnumber (safeint, ≥ 0)
hasServerCodebooleanoptional · default false
provenance{ state: ‘unsigned’ | ‘verified’ | ‘content-mismatch’ | ‘wrong-signer’ | ‘malformed’; standing: ‘unsigned’ | ‘first-sight’ | ‘known’ | ‘rotated’ | ‘changed’; did: string (regex) | null; signedAt: ISODate | null; pluginId: string (regex) | null; version: string (regex) | null; scope: string; knownDid: string (regex) | null; knownSince: ISODate | null; scopeDid: string (regex) | null; scopeSince: ISODate | null; scopeConflict: boolean; reason: string; refused: boolean; needsAcknowledgement: boolean } | nulloptional · default null

ModerationState

The review queue’s state for one listing. flagged is not an accusation, it is the resting state: a module listing is flagged the moment it is created and nothing but an owner’s decision moves it. rejected keeps the row so the same package cannot be re-listed and quietly forgotten about.

'flagged' | 'approved' | 'rejected'

ListingModeration

FieldTypeNotes
listingIdId
stateModerationState
notestringoptional · default ''
flaggedAtISODate
decidedById | nulloptional · default null
decidedAtISODate | nulloptional · default null

ModerationItem

One row of the owner’s review queue: the listing, its state, and what its package declares.

FieldTypeNotes
listingListing
moderationListingModeration
declarationModuleDeclaration | nulloptional · default null

ModerationDecisionRequest

FieldTypeNotes
decision'approve' | 'reject'
notestring (overwrite, max 400)optional · default ''

Lineage

Where a delivered thing came from. A purchased generator, role card, theme or module is a CLONE, and until now the clone knew nothing about its origin: two identical generators in a Studio, one authored and one bought, were indistinguishable, and an update to the original was undiscoverable. One row per delivered artifact fixes both — forkedFrom is the source id on the seller’s side (null when the listing had no ref and the delivery was built from the listing itself, which is an original, not a fork).

FieldTypeNotes
idstring
kindListingKind
ownerIdId
forkedFromstring | nulloptional · default null
listingIdId
orderIdId
sellerIdId
sourceVersionstring (regex) | nulloptional · default null
createdAtISODate

LineageView

Lineage plus what the origin looks like NOW, which is the half that makes an update discoverable.

FieldTypeNotes
lineageLineage
listingListing | nulloptional · default null
latestVersionstring (regex) | nulloptional · default null
updateAvailablebooleanoptional · default false