# Title Check — Pre-booking Preview Flow (finalized / airtight) Status: draft for eng scoping · Android-first · verified against `current_backend/title_checker` + `esearch`. ## 0. The one inviolable rule **Booking must never depend on the preview succeeding.** 77% of buyers convert same-day; a flaky portal must never block payment. Every free source is *best-effort*; the CTA is always live. Preview enriches trust, it does not gate purchase. --- ## 1. Data sources — final tiering (code-verified) | Source | Preview tier | Mechanism | Reliability | Cost | |---|---|---|---|---| | **Chawadi** | Free | warehouse Postgres (`chawadi_warehouse.py`), keyed by `village_code` | ✅ solid (no live portal) | ₹0 | | **7/12** (owners/mutations/encumbrances) | Free · best-effort | Bhulekh scrape OR LiveSatbara ₹15 → `extract_info` (Gemini) | ⚠️ portal-flaky | ₹0 (Bhulekh) / ₹15 (LiveSatbara) | | **Index II raw list** | Free · best-effort · **unverified** | free portal `freesearchigrservice` (legacy `esearch` app), relevance step **skipped** | ⚠️ flaky (`"FreeSearch Portal Issue"`) | ₹0 | | Court cases | **Paid only** | eCourts + captcha, ≤400 searches, owner-keyed | ❌ heavy/flaky | paid | | Verified Index II copies | **Paid only** | paid eSearch download | — | ₹100/doc | | ferfar detail docs | **Paid only** | eFerfar pipeline | — | paid | --- ## 2. Preview state machine (airtight) Each source resolves **independently** and reports its own status. The UI renders whatever is ready and shows graceful placeholders for the rest. Per-source status enum: `ok | empty | pending | not_found | unavailable | low_confidence` ### Acquisition ladder — 7/12 (the variable under experiment; see §6) ``` 1. CACHE HIT (extraction JSON by survey_no, TTL) → serve, ₹0, ~instant 2. CACHE MISS → run the ACTIVE experiment arm (§6) 3. arm source NOT_FOUND / PORTAL_DOWN → status:pending, show "fetching in your report" 4. extraction OK but low field confidence → status:low_confidence, show counts only (no redacted rows) 5. any success → cache JSON, serve ``` Chawadi: cache/warehouse → `ok`; if village_code unmapped → `empty`. Index II raw: free-search → `ok`; portal issue → `unavailable` (silently omit the card, never error). ### UI rendering rules - `ok` → render card with counts (+ redacted detail on tap). - `pending` / `unavailable` → render a quiet "checked in your report" chip, **not** an error. - Never show a spinner that can hang forever: each source has a hard timeout → falls to `pending`. - Clearance meter counts only sources that resolved; it never claims "clear" for a source that didn't run. --- ## 3. Final endpoint contract (temporary → permanent) `POST /api/v1/title-check/preview/` · idempotent by survey_no · cacheable **Request** ```jsonc { "district":"पुणे", "taluka":"हवेली", "village":"वाघोली", "survey_no":"142/2" } ``` **Response** (partial-friendly — any block may be `pending`) ```jsonc { "survey_no":"142/2", "village":"Wagholi", "generated_at":"…", "cache":{ "hit":true, "age_days":3 }, "clearance":{ "resolved":3, "clear":1, "attention":2 }, // only resolved sources "sources":{ "chawadi": { "status":"ok", "count":1, "pending_mutation":true }, "seven_twelve":{ "status":"ok", "arm":"bhulekh_gemini", "owners":{"count":2,"masked":["R••••• P•••••"]}, "mutations":{"count":3,"range":"2009–2024"}, "encumbrances":{"count":2,"active_loan":true}, "confidence":0.91 }, "index2_raw": { "status":"unavailable", "verified":false } // omitted from UI }, "cost_incurred":{ "seven_twelve_rs":0 } // audit trail for ₹15 gating } ``` Async variant: `202` + `{poll_url}` or push; client shows the §2 "checking…" state meanwhile. --- ## 4. Caching & cost control (airtight) - **Extraction JSON cached by `(district,taluka,village,survey_no)`**, shared across all users. Land records change slowly → TTL 30–90d (config). This is what makes repeat views instant and amortizes any ₹15. - **₹15 LiveSatbara is gated**: only on cache-miss **and** an intent signal (card tap / explicit "get full report"), never on passive screen view. Rate-limit per user/session. *(This gating only applies in Arm B, where we subsidize the digital doc — see §6.)* - Chawadi already fresh (warehouse). Index II raw never cached long (flaky + unverified) — short TTL or none. - Every ₹-spending call writes `cost_incurred` for reconciliation. --- ## 5. Edge cases (must all be handled) - Survey/village unmapped → `sources.*: not_found`, still allow booking ("we'll resolve it in your report"). - All free sources down → preview shows only the value prop + CTA; booking unaffected. - Duplicate/parallel requests for same survey → dedupe on the in-flight key. - Encumbrance/court "found" wording is always **owner/plot-scoped truthfully** (person-keyed caveat for anything owner-derived). - Unverified Index II always carries the `unverified` label; never folded into the clearance "clear" count. --- ## 6. 7/12 preview experiment — three arms (customer-cost × document source) The variable is **who pays for the digital 7/12 and which document powers the preview**. Extraction is our Gemini OCR in all three arms. This is a **unit-economics** experiment (where the ₹15 lands), not an extraction-quality one. | Arm | Customer pays? | 7/12 that powers the preview | Our cost / preview | Fidelity | |---|---|---|---|---| | **A · Free / Bhulekh** | No | free Bhulekh scanned image → Gemini OCR | ₹0 | lower — old/scanned, flaky portal | | **B · Free / subsidized digital** | No | LiveSatbara signed PDF — **we absorb ₹15** | ₹15 (cached + intent-gated) | high — current, clean | | **C · Paid digital** | **Yes** | LiveSatbara digital satbara — **customer pays** | ₹0 to us (pass-through) | high, but no rich free preview | **The bet each arm makes** - **A** — cheapest to us; can a lower-fidelity free scan still convert? - **B** — subsidize the good document to maximize trust & conversion; worth ₹15 CAC *only if* incremental margin > cost. - **C** — don't subsidize; the customer pays for the digital satbara (≈ status quo). Free preview stays thin: Chawadi + Index II raw (+ maybe Bhulekh counts). **Primary metric — contribution margin per user** = `price × P(paid) − our_fetch_cost`, by arm. Conversion alone is misleading: B can win on conversion yet lose on margin. **Secondary** — refund/dispute rate (fidelity risk: a mis-read Bhulekh preview erodes trust), coverage / NOT_FOUND, p95 latency. **Arm B economics guardrails (so ₹15 doesn't bleed)** - Cache extraction by survey_no → pay ₹15 **once per plot**, amortized across all viewers (popular surveys ≈ free). - Gate the ₹15 fetch behind an **intent signal** (card tap / "get report"), never a passive view (73% bounce). - Rate-limit per user / session / day; every spend logged to `cost_incurred`. **Method** 1. **Offline fidelity + coverage bench** — A vs B on a labeled survey set: does Bhulekh OCR misread enough (owners, amounts) to hurt trust or drive refunds? 2. **Live conversion + margin test** — flag-route A / B / C with a **real control holdout** (learn from last time). Decide on **contribution margin**, not conversion. 3. **Likely outcome to validate** — A or B powers the *free preview*; C's paid digital is the *report* deliverable. They can coexist: a cheap free preview + an accurate paid report.