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
| Field | Type | Notes |
|---|---|---|
id | Id | |
sellerId | Id | |
kind | ListingKind | |
refId | Id | null | |
title | string | |
description | string | |
cover | string | null | optional · default null |
priceCredits | number (≥ 0) | |
rating | number (≥ 0, ≤ 5) | optional · default 0 |
ratingCount | number | optional · default 0 |
sales | number | optional · default 0 |
tags | string[] | optional · default [] |
status | 'active' | 'paused' | 'sold_out' | 'in_review' | optional · default 'active' |
brief | string | null | optional · default null |
createdAt | ISODate |
Order
| Field | Type | Notes |
|---|---|---|
id | Id | |
buyerId | Id | |
listingId | Id | |
sellerId | Id | |
credits | number | |
status | 'pending' | 'paid' | 'delivered' | 'refunded' | |
note | string | optional · default '' |
createdAt | ISODate |
Wallet
| Field | Type | Notes |
|---|---|---|
beingId | Id | |
credits | number | |
lifetimeEarned | number | optional · default 0 |
lifetimeSpent | number | optional · default 0 |
Review
| Field | Type | Notes |
|---|---|---|
id | Id | |
listingId | Id | |
authorId | Id | |
rating | number (≥ 1, ≤ 5) | |
body | string | |
createdAt | ISODate |
Purchase
What a buyer received for an order: the cloned/created thing, or an access grant for skills. (additive, market slice)
| Field | Type | Notes |
|---|---|---|
id | Id | |
orderId | Id | |
buyerId | Id | |
listingId | Id | |
kind | ListingKind | |
deliveredId | string | null | |
createdAt | ISODate |
SellerDashboard
GET /api/market/mine: the seller’s stall at a glance. (additive, market slice)
| Field | Type | Notes |
|---|---|---|
listings | Listing[] | |
sales | Order[] | |
earnings | { total: number; last30d: number; count: number } | |
wallet | Wallet |
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.
| Field | Type | Notes |
|---|---|---|
path | string (min 1, max 200, regex, custom, custom) | |
text | string (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.
| Field | Type | Notes |
|---|---|---|
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 } | |
files | ModulePackageFile[] | |
signature | { payload: { v, type, did, pluginId, version, namespace, manifest, files, signedAt }; signature: { alg, did, sig }; chain: { origin, current, links } | null } | null | optional · default null |
ModuleFileSummary
One file, as the review queue lists it: never its content, only what it is and its digest.
| Field | Type | Notes |
|---|---|---|
path | string | |
bytes | number (safeint, ≥ 0) | |
sha256 | string |
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.
| Field | Type | Notes |
|---|---|---|
pluginId | string (regex) | |
version | string (regex) | |
namespace | string (regex) | |
name | string | |
description | string | |
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 [] |
files | ModuleFileSummary[] | optional · default [] |
totalBytes | number (safeint, ≥ 0) | |
hasServerCode | boolean | optional · 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 } | null | optional · 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
| Field | Type | Notes |
|---|---|---|
listingId | Id | |
state | ModerationState | |
note | string | optional · default '' |
flaggedAt | ISODate | |
decidedBy | Id | null | optional · default null |
decidedAt | ISODate | null | optional · default null |
ModerationItem
One row of the owner’s review queue: the listing, its state, and what its package declares.
| Field | Type | Notes |
|---|---|---|
listing | Listing | |
moderation | ListingModeration | |
declaration | ModuleDeclaration | null | optional · default null |
ModerationDecisionRequest
| Field | Type | Notes |
|---|---|---|
decision | 'approve' | 'reject' | |
note | string (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).
| Field | Type | Notes |
|---|---|---|
id | string | |
kind | ListingKind | |
ownerId | Id | |
forkedFrom | string | null | optional · default null |
listingId | Id | |
orderId | Id | |
sellerId | Id | |
sourceVersion | string (regex) | null | optional · default null |
createdAt | ISODate |
LineageView
Lineage plus what the origin looks like NOW, which is the half that makes an update discoverable.
| Field | Type | Notes |
|---|---|---|
lineage | Lineage | |
listing | Listing | null | optional · default null |
latestVersion | string (regex) | null | optional · default null |
updateAvailable | boolean | optional · default false |