WREA Property Estimate Tool — Production Build Specification

Version 1.0 · 2 July 2026 · Prepared for Thomas (CEO) and Felix (dev) · Companion to the Seller Email Nurture Operating System spec (same date)

Scope: productionising the Property Estimate V3 prototype into the WREA portal (wrea-multi-project/wrea_main) as a full-funnel lead-generation anchor — endpoint architecture, abuse controls, data minimisation, conversion design, compliance copy, nurture integration, testing, and phased launch.

Evidence base: line-level review of the V3 prototype (server.mjs, src/main.jsx, built bundle scan) and a code-level pass over the portal (routes, throttling, reCAPTCHA, CORS, lead flow, valuation services, caching, autocomplete) on 2026-07-02.

How to read this document Thomas: §1–§3 (context, data-rights posture, decisions), §8 (conversion design), §12 (compliance copy), §14 (phases + launch checklist), §16 (risks). §2 records the two items you own personally. Felix: §5–§7 (current-state findings with file:line, architecture, backend implementation), §9–§11 (abuse, tracking, minimisation), §13 (testing), §14 (build order). Every claim about existing code carries a path — verify against pre_production before building; this spec was written from a read-only pass on 2026-07-02.

1. Context and strategy

The V3 prototype (artifacts/wrea/valuation-prototypes/wrea-property-estimate-tool-v3-2026-06-30/) is a WREA-branded React/Vite app behind a small Node server: landing → address → 5s loading → results (range, comparables, agent questions) → contact gate. The product shape is good and converts the right way: the estimate is positioned as a benchmark, and the CTA reframes the user's question from "what's the number?" to "which agent can explain and defend the number?" — a wedge LocalAgentFinder and OpenAgent don't own.

Review verdict (delivered to Codex 2026-07-02): the product ships; the prototype's plumbing does not. The raw GET /api/property-estimate?address=… endpoint is scrapeable, every lookup makes two live upstream calls with zero caching, and the portal's existing patterns — if copied — would reproduce the problem (§5.2). This spec is the production path: same product, hardened rails.

Strategic role (from the production review brief): acquisition landing page for paid/organic/competitor-intent traffic; lead qualification; sales enablement; email-capture engine for the nurture operating system; evergreen standalone asset. The keyword opportunity is real: "property value" cluster ≈ 80k+ monthly searches where WREA is absent or weak; OpenAgent's OpenEstimates (118k vol) runs exactly this loop.

2
Upstream calls per uncached lookup today
0
Caching / rate limits in prototype
1,900/mo
"property value estimate online" searches (P1 gap)
66%
Rescue-lane leads with no valid email — this tool is the capture fix

2. Data rights and sourcing posture owner-verified gates

The tool republishes two data classes to anonymous consumers, which is a different rights posture from internal CRM use. Ownership of the two gates is recorded here:

GatePositionOwner
DR1 — Sold-listings comparables (scraped listings table: street address, price, month/year, beds/baths/cars)Thomas will separately verify the listings data is in the public domain before it is used in the tool. Until that verification is recorded, comparables ship in the beta behind a config flag (estimate.comparables_enabled) so the tool can launch with or without them. Fallback if verification narrows scope: suppress street numbers, or gate full addresses behind lead capture.Thomas
DR2 — Estimate source (ODIN) (the private market data lane in the prototype: MARKET_DATA_HOST/PATH, server.mjs:53-67)ODIN is a paid subscription — Thomas pays for this data separately. It is a licensed source, not an unlicensed scrape. Engineering treats it as licensed; the only residual action (with Thomas) is a one-time check that the subscription tier permits consumer-facing display of derived estimates, recorded in this doc when confirmed.Thomas
Engineering consequence Data rights are handled as owner-verified configuration gates, not build blockers. The build proceeds in full; DR1 controls only whether the comparables section renders at launch, and both estimate and comparables sit behind service interfaces (§7) so a source can be swapped or scoped without touching the product.

Independent of rights, the rounding + stable-adjustment layer (§7.4) stays: it prevents the public payload from echoing raw source values exactly, which is good hygiene for any licensed source.

3. Executive decisions taken

Opinionated calls baked into this spec. Veto before build starts — everything downstream depends on them.

DecisionRationale
D1. Keep the React app; mount it as a Vite island in the portal.Rewriting in Blade/jQuery wastes the prototype and slows shipping. Precedent exists: the flow-editor is already a Vite+Vue island (vite.flow-editor.config.js). A Blade wrapper route provides session, CSRF, GTM and brand chrome.
D2. Three POST endpoints with signed single-use candidate tokens; no raw-address GET.The prototype endpoint is a free oracle. Tokens are HMAC-signed, TTL 10 min, session-bound, single-use; provider identifiers never reach the client (§7.2).
D3. Cache-first with a global upstream budget and circuit breaker.Two live ODIN calls per lookup with no cache is a cost and availability hole even with a paid subscription. Redis cache by normalised-address hash (14d TTL); daily call budget (~500/day to start) degrading to the existing suburb-tier fallback (PropertyValuationService).
D4. Value-first, gate the shortlist.Full range + 3 comparables + narrative free; agent shortlist, full comparable set, and emailed report behind contact details; SMS PIN verification before lead handoff to sales/agents, not before the shortlist teaser. Teaser-gating the estimate reads as bait and hurts paid conversion.
D5. Deterministic estimates keyed on the resolved property, not the typed string.The prototype's adjustment factor hashes the raw user input (server.mjs:543,551) — "18 Tooke St" vs "18 Tooke Street" returns different numbers for the same house. Trust-destroying and screenshot-comparable. Key on the resolved provider record id (server-side only).
D6. Turnstile, risk-triggered, never on the first lookup.Portal reCAPTCHA enforcement is commented out repo-wide and only flags score ≤0.1 — don't rebuild on it. Cloudflare Turnstile challenges from the 3rd distinct address per session; first-lookup users (paid traffic) are never challenged.
D7. All rate-limit and estimate caches explicitly on Redis.Portal default cache driver is database; under burst (exactly when limits matter) DB-backed counters fall over. Cache::store('redis') everywhere in this feature.
D8. Nurture integration via the existing nurture spec's contract — no separate drip.The tool emits pvt_events, scoped consent, verified email, and estimate history exactly as specced in reports/wrea-email-nurture-build-spec-2026-07-02.html §12. This tool is that system's email-capture engine.

4. Requirements

4.1 Functional

IDRequirementPhase
R1Public flow: address autocomplete → candidate selection → estimate (range + confidence label + narrative) → comparables (flag-gated per DR1) → agent-question section → shortlist CTA → contact gate → SMS verification → CRM lead.P1
R2No raw-address value endpoint; POST session/token design per §7.2. Provider IDs, source names, raw estimates never leave the server.P0
R3Cache-first lookups: repeat addresses served from Redis without upstream calls; global daily upstream budget with circuit breaker and Telegram alert.P0
R4Same resolved property ⇒ same estimate, regardless of address formatting (D5).P0
R5Comparables use real geographic adjacency (lat/lng radius or suburb adjacency), replacing the prototype's 4-suburb curated map + postcode ±12 fallback.P1
R6Lead creation reuses the capsule→CRM path with a compact valuation summary (range, top-3 comparables, suburb context, agent questions) attached to the CRM lead note.P1
R7Emailed estimate report with magic-link email verification ("save your estimate"), feeding nurture consent scopes.P2
R8Full funnel event tracking per §10, banded dimensions only.P1
R9Graceful no-data / thin-market UX verified in ≥5 non-Newcastle markets before public launch.P2
R10Agent-evidence teaser, three-scenario pricing strip, and suburb fee context on the results page (the "uniquely WREA" layer).P3
R11pvt_events + property_estimates emission per the nurture spec contract (§17).P2

4.2 Non-functional

5. Current state: prototype findings and portal gaps (code-verified 2026-07-02)

5.1 Prototype findings (fix in the port)

#FindingEvidence
F1Raw GET value endpoint, no limits, no sessionserver.mjs:685-698/api/property-estimate?address=…
F2Two live upstream (ODIN) calls per request, zero cachingserver.mjs:662-672 (lookupValue: locations + property detail)
F3Estimate varies with address typing (non-deterministic per property)server.mjs:543,551stableFactor(`${address}:mid`) keyed on raw input
F4Header phone is 133 033 — not WREA's number (portal uses 1300 665 557 in 32 views); competitor-adjacent leftoversrc/main.jsx:52-54
F5Prefilled test contact (wrea-review@example.invalid)src/main.jsx:398-403
F6Unsubstantiated ★★★★★ trust row — ACL fake-rating exposure and LAF-style furnituresrc/main.jsx:76
F7Hero "Know your likely sale price" contradicts the estimate positioning; "Estimate benchmark from WREA market data" misattributes a licensed third-party sourcesrc/main.jsx:68,219
F8Comparables geography: curated map covers 4 Newcastle suburbs; fallback postcode ±12 is geographically wrong in most of Australiaserver.mjs:222-228, 393-394
F9Comparables query shells out to the mysql CLI with hand-escaped string SQL and MYSQL_PWD in env — prototype-only patternserver.mjs:416-429 (escaping via sqlString, :183)
F10Internal wording leaks to UI ("local read-only data lane"); "prototype" strings in copyserver.mjs:496; src/main.jsx:349-351,365
F11Fixed 5s artificial loading stepsrc/main.jsx:6,424-426
F12Clean: built bundle contains no provider/source terms (scan hit only "WREA"/"Real Estate Agent" substrings); public payload has no provider IDs or raw valuesdist scan 2026-07-02; server.mjs:622-641 (publicPayload)

5.2 Portal gaps (the patterns NOT to copy, and what to add)

#GapEvidence
G1Closest existing analog GET /api/v1/property-value/{id}/{campaignId?} is live with no throttle, no auth, no captcha, no referer checkroutes/web.php:334 (bare group at :268) → ApiController@calculatePropertyValue
G2reCAPTCHA verification exists but enforcement is commented out repo-wide; threshold only score ≤0.1HomeController.php:36-59 (~:334), ApiController.php (~:170)
G3Main lead FormRequest has empty validation rules (client-side only)app/Http/Requests/ApiRequest.phprules() returns []
G4CORS is Access-Control-Allow-Origin: * on the whole api group — not a scraping controlapp/Http/Middleware/CorsMiddleware.php
G5Default cache driver is database; throttle counters share it (falls over under burst).env.example — Redis configured but not default
G6track/* endpoints have no throttleroutes/api.php:65-75
G7Reusable and good: SMS PIN with attempt lockout, Experian + blacklist, capsule→CRM flow, Places autocomplete, Cache::remember pattern, PropertyValuationService suburb tiers, GTM conventions§19 reference table

6. Target architecture

Frontend island — V3 React app, new Vite entry, mounted on a Blade route (session + CSRF + GTM inherited); Google Places autocomplete on the address step
Public copy per §12. No provider terms in bundle (release-gate grep, N2).
API layerPOST /api/v2/estimate/search · POST /api/v2/estimate/value · POST /api/v2/estimate/lead (api group: throttle + CORS; plus per-route throttles and session binding)
Candidate tokens: HMAC-signed, 10-min TTL, single-use, session-bound. Raw addresses only in search; value takes tokens only.
Service layerPropertyEstimateService (ODIN wrapper + rounding/adjustment) · ComparableSalesService (remote_mysql, parameterised, DR1 flag) · EstimateSessionService (tokens/limits) · EstimateBudget (daily cap + breaker)
Both data sources behind interfaces — swappable/scopable without product changes (§2).
Cache & state (Redis, explicit store) — estimate cache by normalised-address hash (14d) · candidate records (10m) · token tombstones · rate-limit counters · budget counters
Cache hits bypass upstream and don't count against the budget.
Fallback tier — on upstream failure/budget exhaustion: PropertyValuationService suburb/type/bedroom tiers → state defaults, with lower-confidence copy (N1)
Lead & nurture — capsule→CRM lead + valuation summary note · SMS PIN verify (existing) · pvt_events + property_estimates emission (§17) · GTM + server-side track events (§10)

7. Backend implementation detail

Laravel 8.x / PHP 8.2, branch pre_production. New namespace App\Services\Estimate\. FormRequests with real rules — do not extend or copy ApiRequest (G3).

7.1 Endpoints

POST /api/v2/estimate/search        throttle:10,1 + session
  in : { query: string (5..120 chars) }
  do : validate; call ODIN locations lookup (cache 10m by normalised query);
       score candidates (port server.mjs:87-138); store top 5 candidate records
       server-side in Redis (10m TTL); mint one HMAC token per candidate
  out: { candidates: [ { displayAddress, token } ] }        // nothing else

POST /api/v2/estimate/value         throttle:5,1 + session + abuse layer (§9)
  in : { token }
  do : verify HMAC + TTL + session binding + single-use tombstone;
       load candidate record (server-side) → resolved provider record id;
       estimate = Redis cache by sha1(normalised_address) ??
                  (budget.consume() ? ODIN property detail : suburb fallback tier);
       apply rounding + stable adjustment keyed on provider record id (D5/R4);
       comparables = config('estimate.comparables_enabled')
                     ? ComparableSalesService::for(property) : null;   // DR1
  out: transformer whitelist (§11.1) only

POST /api/v2/estimate/lead          throttle:10,1 + session
  in : { estimateSessionId, firstName, lastName, email, phone, consent{} }
  do : real validation rules; Experian + blacklist (existing); capsule→CRM
       with valuation summary note; SMS PIN via Api\LeadController flow;
       emit pvt_events (value_requested/viewed already logged; lead events here)
  out: { leadId, pinRequired: true }

7.2 Candidate token design

7.3 Comparables (port + fix of server.mjs:262-501)

7.4 Rounding, adjustment, confidence

7.5 Budget and circuit breaker

EstimateBudget (Redis counters, daily key)
  consume(): INCR estimate:budget:{Ymd}; if > cap (config, start 500) → false
  breaker : 5 consecutive upstream failures → open 10 min → fallback tier
  alerts  : 80% budget, breaker open, budget exhausted → Telegram (existing rails)

7.6 Persistence

8. Frontend, UX and conversion design

8.1 Mounting

8.2 Flow and gating (D4)

 LANDING            ADDRESS              LOADING (~3s adaptive)      RESULTS (free)
 hero (fixed copy)  Places autocomplete  real progress states        range low/likely/high
 single CTA    →    candidate pick   →   "matching property…"    →   confidence label
                                         "checking recent sales…"    3 comparables (flag DR1)
                                                                     agent questions
                                                                     [Compare local agents]
                                                                          │
                                                              CONTACT GATE (name/email/mobile,
                                                              APP notice + scoped consent)
                                                                          │
                                                              SMS PIN VERIFY (existing flow)
                                                                          │
                                                              SHORTLIST + full comparables
                                                              + emailed report (P2 magic link)

8.3 UX changes from prototype

ChangeDetail
Loading step ~3s adaptive (was fixed 5s, F11)Minimum 2.5s to let the credibility copy land; extends with real latency; progress rows switch on actual pipeline stages. 5s is too long for paid mobile bounce behaviour.
Copy fixes (F4–F7, F10)Per §12 replacement table. Phone, stars, hero line, "WREA market data" attribution, prototype strings, internal wording.
Edit-address affordanceThe results header shows the resolved address with the edit icon (main.jsx:200) — wire it to return to the address step (prototype renders the icon but has no handler).
No-data path (R9)Keep UnavailableStep, add the suburb-tier fallback presentation before giving up: a suburb-level "Indicative" range still converts and still gates the shortlist.
Mobile-first QAPrimary traffic is paid mobile; the estimate grid and comparable rows must be verified at 360px. Existing CSS is close but untested on real devices.

8.4 Uniquely-WREA layer P3 (R10)

9. Abuse controls

Layered; starting numbers to tune with real traffic. All counters on Redis (D7). Never rely on: CORS, CSRF-on-GET, client-side encoding, or any single control (the brief's list — endorsed).

ControlSetting
Per-session value lookups5/day; 3 distinct addresses/session (a genuine seller checks their own home, maybe one more)
Per-IP (sliding window)10 lookups/hr, 30/day — NAT-tolerant; exceeding ⇒ challenge, not block
Token integritySingle-use tombstones, 10-min TTL, session binding; replay ⇒ generic error + abuse counter
Cache-first economicsCache hits don't touch upstream or budget; popular addresses cost nothing to re-serve
Global upstream budget~500 ODIN calls/day initial; breaker to suburb fallback; Telegram alerts at 80%/100% (§7.5)
Challenge layer (D6)Cloudflare Turnstile on the address step from the 3rd distinct address per session, or on per-IP hourly breach. Never on first lookup. Existing reCAPTCHA v3 signal optional, never the gate.
Lead-form protectionHoneypot field + minimum-time-to-submit + existing Experian validation + blacklist (HomeController@checkBlacklist) + PIN attempt lockout (extend the id-keyed pin_attempts_{id} with an IP-keyed cap)
MonitoringAlert on distinct-address velocity per IP/session, token replays, budget thresholds, breaker state; daily abuse digest to Telegram

10. Tracking and full-funnel measurement

11. Data minimisation rules

11.1 Response whitelist (transformer-enforced, N2)

{
  available, formattedAddress, suburb, stateCode, postCode,
  category,                      // consumer label, e.g. "Detached home"
  beds, baths, carSpaces,
  estimate: { low, likely, high, confidence, display{} },   // rounded+adjusted only
  narrative,
  comparableSales: { available, summary{count, displayMedianPrice, suburbs, message},
                     rows[{address*, suburb, beds, baths, cars,
                           displayPrice, displaySoldMonth, matchReasons[]}] }
}   // * address display scope subject to DR1 verification outcome

Excluded always: provider names/IDs/paths, raw estimate values, match scores, raw confidence strings, internal query detail, land size, exact sale dates.

11.2 Logs, analytics, bundle

12. Compliance and copy rules

Current (prototype)Production replacement
"Know your likely sale price before you choose an agent" (main.jsx:68)"Understand your home's likely price range — then compare the agents who can defend it."
"Estimate benchmark from WREA market data" (main.jsx:219)"WREA market benchmark, based on recent market data" (no proprietary-data claim over a licensed source)
★★★★★ trust row (main.jsx:76)Remove, or replace with a substantiated, sourced rating (real count + platform)
tel:133033 (main.jsx:52)tel:1300665557 (portal standard)
Prototype/privacy strings ("details stay local in this prototype…", main.jsx:349-351)APP collection notice + scoped consent checkboxes (marketing vs service contact) + real Terms/Privacy links; state SMS verification will occur (the prototype's note is good — keep the honesty)

Standing disclaimer (under the estimate, short form)

Approved short form"This is an automated estimate based on recent market data — a starting range for your agent conversation, not a valuation or a promise of sale price." Longer T&Cs paragraph: automated model, accuracy varies with data availability, not a substitute for a licensed valuation or agent appraisal.

13. Testing strategy

Portal conventions: PHPUnit in tests/, Playwright in e2e-tests/tests/portal/ and …/cross-system/ (capsule POST interception pattern; d_addr param skips the Places step).

13.1 Unit (services)

13.2 Feature (HTTP)

13.3 E2E (Playwright)

13.4 Release gates (CI)

14. Phased delivery plan and launch checklist

Phase 0 Rails — ~3–4 dev days

Exit: unit/feature tests green; a scripted scrape attempt (100 addresses) is throttled/challenged and costs ≤ budget; no provider terms in any response.

Phase 1 Controlled beta — ~4–6 dev days

Exit: full-funnel E2E green; first real leads carry valuation summaries into CRM; funnel events reconcile between GA4 and server-side track; zero provider leakage in production responses (spot-checked).

Phase 2 Public launch — ~1–1.5 dev weeks

Exit: public traffic, complaint/abuse metrics within thresholds; nurture events flowing; weekly readout includes the tool funnel.

Phase 3 Uniquely-WREA layer + growth — ongoing

Launch checklist (condensed)

Before first production testBefore public launchCan follow after launch
POST/token endpoints only · Redis caches/limits/budget · transformer whitelist + tests · CI grep gates · prototype artefacts purged (F4–F7, F10) · determinism fix (F3) · parameterised comparables (F9) · disclaimer copy · legacy route throttled · DR1 flag wiredTurnstile + alerting · real adjacency replaces postcode±12 (F8) · thin-market UX in ≥5 markets · PIN + capsule E2E · CRM summary verified · privacy/T&Cs reviewed · load test · DR1 verification recorded · DR2 tier check recordedagent-evidence teaser · scenario strip · fee context · emailed-report A/Bs · suburb embeds · retargeting audiences · licensed-AVM swap option

15. Dogfooding and rollout protocol

  1. Local first: the prototype stays as the reference implementation; the portal build is verified against it on the sample property (18 Tooke Street, Bar Beach — expected range parity after the determinism fix).
  2. Staging/allowlist: Thomas + Felix run the full funnel on their own addresses; verify CRM lead note content, PIN SMS, GTM events in the GA4 debug view.
  3. Scrape-yourself test: before any public exposure, run the scripted 100-address scrape against staging and confirm throttles/challenges/budget behave — this is the release gate for §9, not an afterthought.
  4. One ad group beta: low-budget paid-search ad group → unindexed URL; watch conversion vs the standard funnel, upstream spend, abuse digest, and consultant feedback on lead quality for one week.
  5. Kill criteria (pre-agreed): upstream budget exhausted 2 days running from non-converting traffic; complaint about estimate accuracy from a listed vendor; any provider-term leakage; conversion below the standard funnel after 200 clicks — pause and review, don't tune live.
  6. Weekly readout in the growth report: funnel counts, cost per verified lead vs the standard funnel, estimate→lead rate by value band.

16. Risks and safeguards

RiskSafeguard
Scraping / free-oracle abuseToken design (§7.2), layered limits + Turnstile (§9), scrape-yourself release gate (§15.3)
Upstream cost blowout / ODIN availabilityCache-first + daily budget + breaker + suburb fallback tier (§7.5, N1); alerts to Telegram
Data-rights exposureDR1/DR2 owner-verified gates (§2); comparables config flag; source interfaces for swap/scope; adjustment layer prevents raw-value echo
Provider/source leakageTransformer whitelist + CI grep gates (N2, §13.4); banded analytics only (§10)
Trust damage from inconsistent estimatesDeterminism fix keyed on resolved property (D5/R4) with dedicated test
Wrong-geography comparables outside NewcastleReal adjacency (R5) + ≥5-market QA (R9) before public launch
ACL / misleading-conduct exposureCopy replacement table + disclaimer (§12); no "valuation" noun; no fake ratings; substantiated claims only
Privacy (APPs)Collection notice, scoped consent, hashed addresses in logs, PII only in CRM (§11, N6)
Conversion damage from over-protectionNever challenge first lookup (N3); value-first gating (D4); limits tuned generous-per-IP, tight-per-session
Copying portal anti-patternsG1–G6 documented; real FormRequest rules and Redis stores are spec requirements, not suggestions

17. Email nurture integration

This tool is the email-capture engine for the nurture operating system (reports/wrea-email-nurture-build-spec-2026-07-02.html). Contract (that spec, §12; tables §6.7):

18. Open questions / decisions needed

  1. DR1 outcome — listings public-domain verification result decides comparables display scope (full addresses / street-name-only / lead-gated). (Thomas — in progress per §2)
  2. DR2 tier check — confirm ODIN subscription permits consumer-facing derived-estimate display; record here. (Thomas)
  3. Initial upstream budget — spec says 500 calls/day; confirm against ODIN pricing/quota. (Thomas + Felix)
  4. Turnstile account — Cloudflare already in front of the portal? If not, decide Turnstile-standalone vs staying with reCAPTCHA-but-enforced. (Felix)
  5. Beta traffic source — which paid ad group carries the beta, and what daily click budget. (Thomas)
  6. Suburb fallback presentation — show suburb-tier "Indicative" range when the property lookup fails, or go straight to Unavailable? Spec recommends showing it (N1). Confirm. (Thomas)

19. Appendix: key code references

19.1 Prototype (reference implementation)

ThingWhere
Candidate matching / scoringserver.mjs:87-138
Rounding + adjustment + public estimateserver.mjs:503-575 (fix seed per D5)
Comparables query/scoring/summaryserver.mjs:262-501 (replace CLI + geography)
Public payload shapeserver.mjs:622-641 (basis for §11.1 whitelist)
Flow/state machine + copysrc/main.jsx:393-448; copy fixes at :52-54, :68, :76, :219, :349-351

19.2 Portal (verified 2026-07-02, branch pre_production)

ThingWhere
Legacy unprotected valuation route (throttle it)routes/web.php:334ApiController@calculatePropertyValue (:1221+)
Suburb-tier valuation fallback (reuse)app/Services/PropertyValuationService.php (REA cache → OTH → suburb_info → state defaults, all remote_mysql)
api/v2 throttles + CORSroutes/api.php:16-75; app/Http/Kernel.php:37-41; app/Http/Middleware/CorsMiddleware.php
SMS PIN + lockout (reuse)app/Http/Controllers/Api/LeadController.php (sendPinVerification/verifyPin, pin_attempts_{id} :184-195)
Lead capsule → CRM (reuse)ApiController@postCapsuleTriggerEvent (:691) → App\Services\CapsuleApi; blacklist via HomeController@checkBlacklist
Dormant reCAPTCHA (signal only)HomeController.php:36-59; enforcement commented ~:334 and ApiController ~:170
Places autocomplete (reuse)resources/assets/js/map-search.js:13,77
Cache pattern (reuse, on Redis store)app/Services/SuburbsApi.php:22,91,161 (Cache::remember)
Vite island precedentvite.flow-editor.config.js + package.json flow-editor scripts
GTM include + event conventionsresources/views/includes/analytics.blade.php; data-gtm-event e.g. compare_agents.blade.php:164
E2E patternse2e-tests/tests/portal/, …/cross-system/ (capsule intercept, d_addr skip)

19.3 Related documents

Spec authored by Claude (Fable 5) on 2026-07-02, expanding the adversarial production review delivered the same day, from read-only review of the V3 prototype and the wrea_main codebase. Data-rights posture per Thomas's direction: listings public-domain verification owned by Thomas before use (DR1); ODIN is a separately paid subscription (DR2). Verify code references against the current pre_production branch before implementation. v1.0.