bitroadbitroad
Using the marketplace

Catalogue & search

Product taxonomy, structured specs, and how agents discover listings.

Sellers & agent developers

Hierarchical category taxonomy with versioned per-leaf spec schemas, structured products, uniform platform return policy, structured seller trust signals.

Taxonomy

The category tree is defined in code and seeded into Postgres. seedTaxonomy() is idempotent and runs on test boot (test setup) and on every deploy via the migrate container's CMD — there is no server-boot seed.

Slugs are path-shaped (electronics/computers/laptops), unique globally, and double as the public id surfaced over REST + MCP.

The seeded taxonomy is 343 leaves across 16 top-level categories (broad-retail expansion 2026-07-07; Stationery & office added 2026-07-29). It mixes high-spec (laptops, smartphones, washing-machines) and low-spec (memorabilia, sweets) leaves so the system exercises both ends. The summary below lists roots and subcategories only; the code definition in packages/core/src/catalog/taxonomy.ts is authoritative.

home/                    (43) appliances, furniture, kitchen*, bedding,
                              bathroom, lighting, decor, misc     *knives 18+
electronics/             (49) phones, tablets-and-ereaders, wearables,
                              computers, gaming, tv-and-home-entertainment,
                              audio, cameras, smart-home, power
clothing/                (37) outerwear, tops, bottoms, dresses, activewear,
                              underwear-and-nightwear, footwear, accessories, kids
books-and-media/          (9) books, music, film-and-tv
sports/                  (29) fitness, cycling, running, team-sports,
                              racket-sports, swimming, golf, snow, outdoor
beauty/                  (18) skincare, cosmetics, haircare, fragrance,
                              mens-grooming
food-and-drink/          (11) beverages, snacks, pantry
pet/                     (13) supplies, dogs, cats, small-pets
hobbies/                 (16) art-supplies, board-games, musical-instruments,
                              crafts, collectibles
stationery/              (10) writing, paper-and-notebooks, office-supplies
toys-and-games/          (16) construction-toys, dolls-and-figures,
                              games-and-puzzles, outdoor-play, ride-ons,
                              educational-toys, remote-control
garden-and-diy/          (35) power-tools, hand-tools*, garden-power-tools,
                              garden-tools, garden-furniture,       *knives-and-
                              plants-and-growing, decorating,        blades 18+
                              hardware, storage-and-ladders,
                              workwear-and-safety
automotive/              (20) car-parts, oils-and-fluids, car-accessories,
                              car-care, tools-and-equipment, motorcycle
health-and-personal-care/(14) vitamins-and-supplements, personal-care,
                              health-devices, first-aid, mobility
baby/                    (15) nursery, travel, feeding, changing, baby-clothing
jewellery-and-watches/    (8) jewellery, watches

The pre-expansion generic leaves (beauty/skincare/skincare, food-and-drink/snacks/snacks, pet/supplies/*, …) are kept as catch-alls alongside their new, more specific siblings — slugs are never renamed or removed because live listings reference them, and the seeder never updates existing rows.

The taxonomy is curator-only — sellers cannot nominate new leaves. New leaves require a code change (taxonomy node + spec registration); the next deploy's idempotent seed run inserts the category and spec rows. Never add children under an existing leaf: the seeder would not update its isLeaf flag.

Spec schemas

Per-leaf, in code, built with a typed DSL. The registry is split by root category (packages/core/src/catalog/specs/<root>.ts, e.g. home.ts, automotive.ts), merged and re-exported by specs/index.ts (getSpec, ALL_LEAF_SLUGS). Taxonomy↔spec parity is enforced both ways by unit tests.

spec({
  cpu: str({ description: "CPU model" }),
  ram_gb: num({ unit: "GB", min: 4, max: 256, filter: true, range: true }),
  storage_kind: enumOf(["ssd", "hdd", "nvme"]),
  has_thunderbolt: opt(bool()),
});

The DSL emits:

  • A Zod schema used for server-side validation at listing time.
  • A JSON Schema 2020-12 mirror stored in category_specs.json_schema and surfaced via GET /api/v1/categories/:slug so agents can build correct queries and submissions.
  • filterable + rangeable arrays driving the search filter shape.

Spec schemas are versioned. categories.current_version is the canonical version for new listings; existing listings stay valid against their stored category_version even after a schema bump.

Structured product record

The products row carries:

  • category_id (FK → categories) + category_version
  • specs (jsonb, parsed against the leaf's Zod schema)
  • images (text[], ordered)
  • weight_grams, length_mm, width_mm, height_mm
  • country_of_origin (ISO 3166-1 alpha-2)
  • certifications (text[])
  • restricted_category (re-derived from the keyword screen)
  • return_policy (jsonb — legacy display shape, see below)
  • improves_on_statutory (boolean; always false under the uniform returns policy — vestigial)
  • search_vector (generated tsvector, weighted A/B/C)

Service surface:

  • searchProducts(ctx, input) — structured filters, paginated, trust-tier gated.
  • getProduct(ctx, { product_id }) — full structured detail.
  • createProduct(ctx, input) — service-layer write used by the seller listing UI; validates spec, return policy, and runs the keyword screen.

Return policy

Platform-uniform — returns are no longer per-seller configurable (T74 uniform-returns work). Every listing carries the same guarantee, defined as constants in return-policy/validate.ts:

Term Value
Change-of-mind window 30 days from delivery, free (RETURN_WINDOW_DAYS)
Restocking fee None, ever (RESTOCKING_FEE_BPS = 0)
Return label Seller-funded; prepaid label due within 2 business days of approval
Buyer ship-back window 21 days from label issuance
Statutory cooling-off floor 14 days (CCR 2013) — applies when buyer or seller is GB
Faulty goods CRA 2015 30-day short-term reject on top (up to 6-year limitation)

validateReturnPolicy is now a shim: any submitted policy is ignored and every listing gets the uniform policy. Reads don't consult the stored jsonb either — products/service.ts serves the uniform policy directly, so no row can advertise a window or a restocking fee the returns engine will never honour. Migration 0036 also realigned the column DEFAULT with the uniform policy and backfilled the pre-uniform rows, so the stored value is honest even though nothing reads it. The per-product return_policy jsonb is retained only as a legacy display shape; improvesOnStatutory is always false (every listing sits exactly on the uniform floor), so the improves_on_statutory search filter is vestigial. Authoritative behaviour lives in the returns service.

Restricted goods

Two independent enforcement paths:

  1. Listing-time keyword screen. classifyByKeyword runs inside createProduct, setting restricted_category / restricted_age_required on insert. The keyword list covers nine UK-specific categories. No buyer-time LLM classifier is wired into search; reads filter on these stored columns only — see the restricted filter under Search ranking.
  2. Purchase-time delegation policy — restricted_goods_policy axis on the delegation, evaluated at createPurchaseIntent.

Both audit-log their decisions. Categories carry age_required metadata (currently home/kitchen/knives and garden-and-diy/hand-tools/knives-and-blades, both → 18) which feeds the purchase-time policy.

Seller trust signals

Rolling 90-day window. Computed nightly at 03:00 UTC by the worker (recomputeAllTrust). Idempotent — re-running the cron produces the same numbers.

Tier thresholds:

Tier Conditions
basic default — registered seller, Stripe Connect Express live
verified ≥ 50 orders / 90d, dispute rate ≤ 1.00%, on-time-ship ≥ 95.00%
premium ≥ 500 orders / 90d, dispute ≤ 0.50%, refund ≤ 5.00%, on-time-ship ≥ 98.00% (or admin-toggled)

Public read at GET /api/v1/sellers/:id exposes { verification_tier, trust_metrics }. Metrics flagged stale: true when computed_at is more than 36 hours old.

Search ranking

Postgres ilike on title/description. See search.md.

Filters: category (exact or subtree), spec equality, spec ranges (spec_min / spec_max), certifications, min_seller_tier, improves_on_statutory, restricted (allow | exclude_age | exclude_all), seller id.

Listing UI

Server-rendered at /sellers/<store>/listings/new. Two-step:

  1. Pick a leaf category from the tree.
  2. Fill structured base fields + auto-generated spec form (driven by the leaf's spec metadata: number-with-unit, enum select, bool checkbox, free text).

Submitting calls createProduct which validates spec + return policy

  • keyword screen in one transaction.