PRTOTYPE.COM · Production studioAudit · Build · Maintain
PRoTOTYPE.COM
Case study · Live demonstrator

A rental market that explains itself.

Index is a property-search demonstrator for Siem Reap, Cambodia: 187 curated rentals, an open-street map with a walking-distance datum, and a scoring engine that shows its working. Behind it sits a deterministic ingestion pipeline that reads agency websites, extracts clean records without a single AI API call, and stages everything for one human approval before it ever reaches the catalogue.

Type
Area intelligence · Next.js
Backend
Deterministic intake pipeline
Status
Live · prerendered
187 listings scored
0 LLM calls · 1 human gate
estate.prtotype.com
Index homepage
The brief

One city, four agency sites, no staff.

Siem Reap’s rental market lives in Facebook groups, agency websites and street signs. A relocator asking “where should I live?” gets opinions, not evidence. The agencies have no tooling to show neighbourhood knowledge; they show listings.

Index’s premise: an area-intelligence product that maintains itself. The site keeps a curated seed catalogue honest, and an ingestion pipeline continuously reads agency sites, pulls structured records out of them with deterministic rules (no LLM, no API key, no per-listing cost), de-duplicates across four key tiers, and routes anything uncertain to a single review queue. The public site only ever shows what passed the gates.

The result is a fast, warm, editorial product that reads like it has a full research team behind it, built and sponsored by prtotype.com as a functional demonstrator of exactly that idea.

“The pipeline never touches the live catalogue. It writes a preview file. A human decides what becomes real.”
187
Listings scored 0–100 against your brief
4
De-dup key tiers, exact URL to fingerprint
0
LLM calls · paid data sources
1
Human review gate before publish
3.2km
Walking-radius datum framing the map
The product

Every answer shows its working.

Warm paper, ink hairlines, mono labels and a scoring engine that treats honesty as a schema decision: a tour of what a visitor actually sees.

estate.prtotype.com/properties
The catalogue
01 / The catalogue

Every listing, scored.

The full directory with faceted filters (area, beds, type, rent ceiling), server-side pagination and a match-score badge on every card. Prices show USD with a £ reference, because the audience is relocating expats.

Server-side paginationFaceted filtersScore badges
estate.prtotype.com/properties/0
Property detail page
02 / The detail page

Honesty you can audit.

Each property carries a tri-state amenity ledger: confirmed (evidence recorded), unconfirmed (never checked), absent (explicit negative evidence). “Not listed” never quietly becomes “doesn’t have it”. Unknown floor area records as zero plus a flag, never a guess.

Tri-state amenitiesEvidence-backedUnknown = 0 + flag
estate.prtotype.com/map
The map with radius spotlight
03 / The map

A map that knows where you are standing.

Every listing plotted on OpenStreetMap, colour-coded by match score, with 1/2/3 km rings from Pub Street and a paper “spotlight” mask that keeps the view inside the 3.2 km walking radius. Pins sync both ways with the side list as you hover.

Walking-distance datumPin–list hover syncOpen data only
estate.prtotype.com/brief
The brief wizard
04 / The brief wizard

Say what you want, see why you got it.

Four steps capture budget, beds, must-haves and deal-breakers, then re-score the whole city against your brief on the server. The weights are data, not secrets: each scoring axis carries named points, so a “why this score?” is answerable in one sentence.

Transparent weightsServer-action scoringNo dataset in your browser
estate.prtotype.com/guide
The area guide
05 / The guide

Neighbourhood knowledge, written down.

A typed-content guide to Siem Reap’s seven zones: character, rent ranges and practical notes, with a sticky table of contents. The context a listings grid can never give you.

Seven zonesTyped content, no CMSSticky ToC
estate.prtotype.com/bot
The catalogue bot
06 / The bot

Ask the catalogue anything.

A rule-based Telegram-style chat answers /top, /areas, /stats and plain questions like “2 bed under $500 in Wat Bo” from the same merged pool the site serves, with a per-IP rate limiter and no LLM dependency.

Same data, zero driftRate-limited APINo LLM needed
Under the surface

Advanced features, quietly done.

The polish a renter never notices but always benefits from: correctness, speed and resilience baked into the architecture.

Client-bundle discipline

The seed dataset never ships to the browser. Brief scoring runs behind a server action, and a grep of the built client chunks proves the data is absent. Pages stay at ~112 kB first load.

Server actions

Escaped everywhere

Listing names come from third-party sites, so they are treated as hostile: JSON-LD is serialised with < escaping, and map popups are built with DOM APIs rather than string HTML. Injection has nowhere to land.

XSS-hardened sinks

Headers that mean it

CSP (tuned until the map tiles actually loaded), frame denial, nosniff, referrer policy and a per-IP rate limiter on the bot API. The admin surface is blocked at the CDN edge entirely.

Edge + app defences

The map spotlight

A paper-coloured radius mask hides everything beyond the 3.2 km walking datum, so the map reads as a tight spotlight on the walkable core instead of a sea of irrelevant pins. Geometry is unit-tested, not eyeballed.

Tested geometry

Honest by schema

Tri-state amenities, unknown-size flags and a status vocabulary automation may never touch are type-level decisions, not copywriting. Every feature inherited the honesty model for free.

Type-level guarantees

Evidence-gated development

Nothing is marked done without a passing test or a live probe: 154 offline tests, fixture-based, zero network in the suite. Four parallel fix agents once landed a full security wave in a day, re-verified 10/10 by an adversarial reviewer.

154 tests · offline
The update engine

Configured once, updated itself.

A seven-stage intake pipeline turns messy agency sites into clean, de-duplicated, human-approved records. Every stage is isolated, and the one irreversible action, publishing, always needs a person.

01
Discover
adapters/*.ts · per-site index walkers
Each agency site (RE.kh, DaBest, IPS Cambodia, FazWaz) has its own adapter that walks the listing index and returns URLs. New sources plug in without touching the rest of the pipeline.
02
Fetch
httpFetcher.ts · SSRF allowlist · throttle
Fetches are pinned to an explicit host allowlist with per-host throttling and Cloudflare back-off. The pipeline is deliberately powerless: it only writes a preview JSON file and never touches the live catalogue.🔒 Hard isolation boundary
03
Extract
extractor.ts · rules + synonym dictionary
Structured fields are pulled out of markup with deterministic rules and a synonym dictionary: zero API cost, zero hallucination risk, fully reproducible. The ReDoS-guarded patterns are all committed fixtures.
04
Normalise
normalise.ts · zones · tri-state amenities
Records are coerced into the canonical schema: neighbourhoods resolved to the seven canonical zones (unmatchable ones flag a concern), rents normalised, and every amenity assigned its honest tri-state: confirmed, unconfirmed or absent-with-evidence.
05
Validate
validate.ts · hard gates
Hard gates before anything proceeds: rent band, bedroom sanity, size honesty (unknown = 0 + flag, never a guess) and a status vocabulary the automation is allowed to use. Anything odd fails loudly instead of quietly passing.✅ Fails loudly, not silently
06
De-duplicate
dedup.ts · union-find · 4 key tiers
A union-find across four match tiers, from exact source URL down to composite fingerprints (address + rent ±5% + beds). Every duplicate keeps its source URL and the exact key tier that merged it, so a human can audit any decision.
07
Review & publish
admin/review · preview JSON
Anything uncertain waits in the admin review queue with its confidence, concerns and extracted fields laid out. One human approval merges it into the live catalogue. Nothing the machine is unsure about reaches the public site unsupervised.✋ Required human approval
scripts/run-ingestion.mjs (the runner)
// discover → fetch → extract → normalise → validate → dedup → review
for (const adapter of ['realestate-kh', 'dabest', 'ips-cambodia', 'fazwaz']) {
  const urls  = await adapter.discover();          // SSRF allowlist + throttle
  const parsed = await fetchAndParse(urls);        // fixtures out, no writes
  const clean = parsed
    .map(extractDeterministic)              // rules + synonyms, zero LLM
    .map(normalise).filter(validate);            // hard gates or fail loudly

  const { unique, duplicates } = deduplicate(clean); // union-find, 4 key tiers
  writePreview(unique, duplicates);                // ← JSON only. never live
}
// a human approves records in the admin queue. that is the whole trick.
Isolated by design
Fetch, extract, validate, dedup and publish are separate stages with hard boundaries, so failures stay contained instead of cascading into live data.
Human-in-the-loop
The pipeline proposes; a person disposes. Automation handles the volume; the single approval gate handles the trust.
Model-agnostic
Extraction is deterministic today: rules and synonyms, zero API cost. The ExtractorProvider interface is the seam where a generative model slots in later through OpenRouter, without rewriting the pipeline around it.
The stack

Built to stay cheap and fast.

A deliberately lean, mostly-static architecture: sophisticated where it counts, boring everywhere else.

Next.js 15
Framework · SSG
273 prerendered pages; server actions keep scoring server-side.
React 19 + TS strict
UI · types
Strict TypeScript with noUnusedLocals on; zero any-casts in the audit.
Tailwind CSS v4
Styling
CSS-first design tokens; no config file to drift.
Leaflet + OSM
Maps
Open data only; no map API key, no per-load cost.
Vitest
Tests
154 offline tests over committed fixtures; suite runs with no network.
Docker + Caddy
Hosting
One container on a Hetzner VPS behind automatic TLS.
Why it holds up

Autonomous, but never unaccountable.

JSON-only
The pipeline can’t write to the catalogue; it physically only emits a preview file.
Staged
Uncertain records wait in the review queue, never auto-published.
Sourced
Every accepted listing keeps its source URL; agency-level contacts are flagged as such.
Static
Prerendered output means no live database in the request path to fail.
A functional demonstrator

See it running.

Index is live: a real, self-maintaining area-intelligence demonstrator built and sponsored by prtotype.com to show what an evidence-gated rental product looks like in production.

Index
Case study · Area intelligence for Siem Reap · Built & sponsored by prtotype.com
Like what you see

Want something like this built for you?

Index went from spec to a live, self-maintaining product: audited, hardened and shipped with the update engine built in. If you have a data-heavy idea that needs the same treatment, let's talk it through.

Fixed prices · Set against a written spec
Book a Production Consultation