Skip to content

Listings and buying

GET /api/market/listings?kind=generator&q=landing+react&limit=60

q is split on whitespace (max 6 terms) and each term is matched against title, description, or the JSON-encoded tags column with a LIKE — good enough for a marketplace this size without standing up a separate search index. Results default to status: 'active' only, ranked by sales, then rating, then newest; pass mine: true to see your own listings regardless of status.

Publishing

POST /api/market/listings
{ "kind": "generator", "refId": "gen_blog", "title": "My tuned blog-post recipe", "description": "...", "priceCredits": 40, "tags": ["writing"] }

Server-side validation beyond the shape: a service listing needs either a brief or a non-empty description (so a purchase always has something to run), and refId for generator/agent/theme listings is checked against real ownership (validateRef()) — you can only ever list a generator, agent, or theme you actually own, checked at insert and at every subsequent edit.

Buying: one atomic transaction

POST /api/market/listings/:id/buy → Order

Everything happens inside one database transaction (db.transaction() in apps/api/src/modules/market/routes.ts), so a partial purchase (credits moved but nothing delivered, or vice versa) is structurally impossible:

  1. Refuse if the listing isn’t active, if you’re buying your own listing, or if you already own a non-service listing (services can be bought repeatedly — they’re commissions, not licenses).
  2. Move credits: debit the buyer’s wallet, credit the seller’s, both wallets’ lifetime totals updated.
  3. Bump the listing’s sales counter.
  4. Deliver — see below.
  5. Insert the Order (status: 'delivered') and a Purchase record.

Only after the transaction commits: both wallets publish wallet.updated over the WebSocket, and a celebration-kind Signal lands on both the buyer’s and the seller’s desktop — “‘Landing page in an hour’ sold for 80 credits” for the seller, a thank-you with a link to what you got for the buyer. Trading is treated as something worth noticing, not a silent database write.

Delivery, by kind

deliver() (apps/api/src/modules/market/delivery.ts) runs inside the same transaction, so a delivery failure rolls the credit transfer back too:

KindWhat happens
generatorA private copy is inserted into your own generators (ownerId = you, isPublic: false, a de-duplicated slug) — a real, independent row, editable without touching the seller’s original
agentSame idea: a private copy of the agent (or, for a citizen’s own listing, a fresh agent carrying their persona) joins your agents
themeA private copy of the theme lands in your theme list, unactivated — you choose when to switch to it in the theme panel
skillYou receive an access grant (a Purchase row with deliveredId = the skill name) and a private agent of your own, already equipped with that skill plus the everyday tools to put it to work, briefed to lead with it — so the purchase is something you can actually run, not just a row
serviceA real Studio Project is created from the listing’s brief and started immediately, exactly as if you’d typed that brief yourself

A listing with no refId (the seed’s own listings, or a seller who described something by hand without linking a real generator/agent/theme) still delivers something concrete — deliver() synthesises a minimal but real two-step generator (research → make) from the listing’s own title and description rather than delivering nothing.

Reviews

POST /api/market/listings/:id/reviews { rating: 1-5, body }

Only buyers can review (hasPurchased() check; the world owner is exempt for testing), one review per buyer per listing (a second POST updates your existing review rather than adding a second one), and a listing’s rating/ratingCount are recomputed from real review rows every time one changes — never a value you can set directly on the listing itself.