dino.place
production

$ cat system.md

For the curious. The full technical colophon.

dino.place — stats
$ dino-place --stats
6
prisma.models
6
api.routes
7
app.pages
151
test.cases
17
test.files
5
migrations

dependencies

framework

  • next14.2
    App Router, server components
  • react18.2
    Static server pages by default
  • typescript5.6
    Strict mode
  • tailwindcssv4
    Utility-first, PostCSS plugin

data

  • postgresqlNeon
    Serverless, branching
  • prisma5.10
    6 models, typed queries

ai

  • gemma3:12bpluto
    Names gear in a video description
  • gemma3:4bpluto
    Classifies a search result as a review

revenue

  • amazon associatestagged links
    Search, product, and cart URLs carry the partner tag

external

  • youtube data v3API
    Finds and titles review videos
  • amazon pa-api 5.0not approved yet
    SigV4 client exists in lib/amazon/, unused in production

hosting

  • vercel
    Deploys on every push to master

testing

  • vitest4.x
    151 tests across 17 files

architecture

architecture.ts
1
cache: search=1h, price=24h, video=24h

Cache-through on every external call: check the database first, hit Amazon or YouTube only on a miss, then store the result. Price TTL is 24 hours because that is the Amazon TOS maximum.

2
asin: parsed by code, never generated

The kit extraction schema (lib/kits/extract.ts) has no ASIN field. The model names an item and points it at a link number; code resolves that number back to an ASIN it already parsed. The model cannot write an ASIN even if a description names one.

3
shortlink: HEAD once, redirect: 'manual'

lib/kits/resolve.ts sends one HEAD request per amzn.to/a.co link and reads the Location header without following it, because requesting amazon.com directly can return a CAPTCHA page at HTTP 200 instead of an error.

4
catalog: CATALOG_WHERE = { source: 'curated' }

An allowlist, not a denylist, in lib/products/visibility.ts. A new source value stays invisible in search, category pages, and featured products until someone adds it here on purpose.

5
affiliateTag: built once, .trim()'d

lib/amazon/affiliate-links.ts is the only place the partner tag is read. A trailing newline in a Vercel env var once passed through URLSearchParams silently and cost affiliate credit on search links while product links kept working — found in production on 2026-09-12.

kits.build()

A kit is a curated gear list tied to one YouTube video. It is built from that video's description, not from the video itself — dino.place has no transcript, only title, channel, and description text.

Links are parsed from the video description as text

lib/kits/links.ts finds every URL and classifies it as an Amazon link, a short link, or neither, before anything touches a model.

Short links resolve one at a time, capped and rate-limited

Up to 40 HEAD lookups, a 2-second gap between them, and the run stops after two failures in a row or a bad-failure ratio — so a batch of dead links fails fast instead of hammering Amazon.

gemma3:12b names the gear, code maps it to an ASIN

The prompt gives the model numbered links and asks which item goes with which number. It never sees a field called ASIN and can't produce one.

An item with no ASIN still publishes

A creator's own shop link, an unresolved shortener, or a non-Amazon store is allowed. That item renders as a plain line on the kit page with no buy button.

planPublish() blocks anything with nothing buyable

A kit with zero valid ASINs across all its items cannot publish. Kits with at least one ASIN still need each pairing confirmed before the item is live.

principles

  • #Every response goes through jsonOk() or jsonErr() from lib/http.ts. No raw Response.json().
  • #Every input is validated with a Zod schema from lib/domain/dtos.ts before it is used.
  • #Every API route is wrapped with withErrorHandling().
  • #A Prisma Decimal arrives as a string. Convert it with Number() before doing math on it.
  • #FTC affiliate disclosure appears on every page that carries an affiliate link.
  • #Amazon prices are cached at most 24 hours — the TOS maximum, not a target.
  • #Migration order follows the change direction: add a column before the code that reads it deploys; drop a column only after the code that read it is already gone.
colophon

$ dino-place --version

Built by one developer, revived from a 2016 MEAN-stack project called Watchville.

Product discovery backed by real video reviews, not sponsored placement.

$ _

Want to see it work?

All this disappears behind a simple search box. Look up a product and see what real reviewers actually said about it.

as an amazon associate, dino.place earns from qualifying purchases