Listings and buying
Search
GET /api/market/listings?kind=generator&q=landing+react&limit=60q 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 → OrderEverything 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:
- Refuse if the listing isn’t active, if you’re buying your own listing, or if you already own a
non-
servicelisting (services can be bought repeatedly — they’re commissions, not licenses). - Move credits: debit the buyer’s wallet, credit the seller’s, both wallets’ lifetime totals updated.
- Bump the listing’s
salescounter. - Deliver — see below.
- Insert the
Order(status: 'delivered') and aPurchaserecord.
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:
| Kind | What happens |
|---|---|
generator | A 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 |
agent | Same idea: a private copy of the agent (or, for a citizen’s own listing, a fresh agent carrying their persona) joins your agents |
theme | A private copy of the theme lands in your theme list, unactivated — you choose when to switch to it in the theme panel |
skill | You 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 |
service | A 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.