
Marketplace Pre Member Personalisation
- 142 installs
- 191 repo stars
- Updated July 24, 2026
- pproenca/dot-skills
marketplace-pre-member-personalisation: A skill for development. This provides functionality for development workflows.
Key points
- marketplace-pre-member-personalisation
Marketplace Pre Member Personalisation by the numbers
- 142 all-time installs (skills.sh)
- +6 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #2,579 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/pproenca/dot-skills --skill marketplace-pre-member-personalisationAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 142 |
|---|---|
| repo stars | ★ 191 |
| Last updated | July 24, 2026 |
| Repository | pproenca/dot-skills ↗ |
How do I use marketplace-pre-member-personalisation for development tasks?
Use marketplace-pre-member-personalisation for development tasks
Who is it for?
Best when you're working on backend & apis and need structured help with marketplace-pre-member-personalisation.
Skip if: Teams with no backend & apis needs, or anyone wanting a generic chat assistant without this specific workflow.
When should I use this skill?
When you need to use marketplace-pre-member-personalisation for development tasks, or when marketplace-pre-member-personalisation: a skill for development. this provides functionality for development workflows.
What you get
Structured output aligned to marketplace-pre-member-personalisation: marketplace-pre-member-personalisation.
Files
Marketplace Engineering Two-Sided Pre-Member Personalisation Best Practices
Comprehensive design and diagnostic guide for the pre-member journey of a two-sided trust marketplace. Covers anonymous signal inference, side-specific validation (what pet owners and pet sitters each need to see before paying), information-asymmetry closure, progressive profile building, social proof, conversion psychology, onboarding intent capture, identity stitching, and pre-member measurement. Contains 53 rules across 10 categories, ordered by cascade impact, every rule grounded in published consumer-trust and decision research.
When to Apply
Reference this skill when:
- Designing or reviewing the anonymous landing page and first-render experience
- Choosing what to show a visitor before they have registered or paid
- Designing the onboarding flow and deciding which questions to ask in what order
- Planning the paywall moment — timing, copy, triggers, price anchoring
- Diagnosing a conversion funnel that is leaking between visit and paid membership
- Choosing how to persist visitor state across the anonymous → registered → member transition
- Measuring pre-member experiments and deciding whether to ship an intervention
- Answering "what does a pet owner or sitter actually need to believe before paying?"
This skill is the precursor to marketplace-personalisation and marketplace-search-recsys-planning. Start here for anything pre-paid-membership; hand off to those two skills at the paid-member boundary.
Research foundations
Every rule in this skill is grounded in published research on consumer trust, decision-making under risk, marketplace economics, and experimentation:
| Research source | What it informs |
|---|---|
| Cialdini — Influence | Social proof (specific beats aggregate), similarity principle, commitment |
| Kahneman & Tversky — Prospect Theory | Loss aversion, price anchoring, risk framing |
| Roth — Who Gets What and Why | Matching-market dynamics, two-sided acceptance rates, cold-start penalty |
| Fogg — Behavior Model | Motivation × ability × trigger, paywall timing |
| Bandura — Self-Efficacy Theory | First-stay path design, concrete-step persuasion |
| Slovic — Affect Heuristic | Risk overweighting, safety-signal prominence |
| Nielsen Norman Group | Form design, trust, review credibility |
| Trope & Liberman — Construal Level Theory | Psychological distance, local proof |
| Ein-Gar, Shiv, Tormala — Blemishing Effect | Mixed-review credibility |
| Small & Loewenstein — Identifiable Victim Effect | Named-person vs statistic evidence |
| Green & Brock — Narrative Transportation | First-experience stories |
| Kohavi — Trustworthy Online Experiments | Primary outcomes, proxy metrics, segmentation |
| Radlinski & Craswell — Optimized Interleaving | Fast ranking experiments |
| Airbnb / DoorDash engineering | Two-sided marketplace ranking and search |
Rule Categories
Categories are ordered by cascade impact on the pre-member conversion journey:
| # | Category | Prefix | Impact |
|---|---|---|---|
| 1 | Anonymous Signal Inference | signal- | CRITICAL |
| 2 | Pet Owner Validation and Trust | owner- | CRITICAL |
| 3 | Pet Sitter Validation and Opportunity | sitter- | HIGH |
| 4 | Information-Asymmetry Closure | gap- | HIGH |
| 5 | Progressive Profile Building | profile- | MEDIUM-HIGH |
| 6 | Social Proof and Lookalike Cohorts | proof- | MEDIUM-HIGH |
| 7 | Personalised Conversion Triggers | convert- | MEDIUM-HIGH |
| 8 | Onboarding Intent Capture | onboard- | MEDIUM |
| 9 | Identity Stitching | stitch- | MEDIUM |
| 10 | Pre-Member Measurement and Experimentation | measure- | MEDIUM |
Quick Reference
1. Anonymous Signal Inference (CRITICAL)
- `signal-extract-role-from-url-and-referrer` — side inferred from URL path before first render
- `signal-infer-geography-with-confidence` — geo-IP with confidence, not false certainty
- `signal-capture-entry-point-metadata` — UTM, referrer, landing path persisted per session
- `signal-use-anonymous-session-tokens` — session-level identity from the first request
- `signal-classify-inbound-intent` — transactional vs investigative vs curiosity
- `signal-separate-raw-from-derived` — raw signal plus versioned derived features
2. Pet Owner Validation and Trust (CRITICAL)
- `owner-show-specific-local-reviews` — identifiable-victim social proof, not aggregate stats
- `owner-display-honest-local-availability` — honest liquidity beats inflated counts (expectancy-violation research)
- `owner-surface-safety-guarantees-prominently` — insurance and coverage above the fold (Slovic affect heuristic)
- `owner-rank-sitters-by-pet-match-experience` — feasibility by pet type, not global popularity
- `owner-demystify-effort-explicitly` — explicit time budget beats aspirational copy (Fogg)
- `owner-anchor-cost-against-local-alternative` — local kennel price as anchor (Kahneman)
3. Pet Sitter Validation and Opportunity (HIGH)
- `sitter-show-inventory-in-target-destinations` — target-specific supply, not global counts
- `sitter-be-honest-about-first-stay-competition` — cohort-specific acceptance rates
- `sitter-provide-concrete-first-stay-path` — five-step path (Bandura self-efficacy)
- `sitter-show-typical-daily-commitment` — explicit hours and walks, not "varies"
- `sitter-rank-stays-by-travel-goal` — goal-aware ranking
- `sitter-disclose-hidden-costs-transparently` — food, utilities, transport (Edelman trust research)
4. Information-Asymmetry Closure (HIGH)
- `gap-warn-about-cold-start-penalty` — first transaction is the hardest; say so
- `gap-surface-lead-time-reality` — median booking advance per destination
- `gap-display-acceptance-rate-for-profile-shape` — cohort acceptance rate before paying
- `gap-route-unworkable-segments-to-alternatives` — decline payment rather than sell false hope
- `gap-surface-seasonal-supply-constraints` — seasonal curves with visitor month highlighted
- `gap-link-to-realistic-first-experience-story` — narrative transportation with honest friction
5. Progressive Profile Building (MEDIUM-HIGH)
- `profile-build-incrementally-on-each-interaction` — click updates profile, next page reranks
- `profile-decay-features-with-inactivity` — exponential decay, 5-minute half-life
- `profile-persist-across-tabs-and-reloads` — server-side session-keyed store
- `profile-surface-confidence-alongside-predictions` — confidence scores next to values
- `profile-reset-on-explicit-role-change` — role switch clears role-specific features
6. Social Proof and Lookalike Cohorts (MEDIUM-HIGH)
- `proof-use-specific-peer-stories-not-aggregates` — named people beat "4.9 stars"
- `proof-match-peer-stories-to-inferred-cohort` — similarity principle
- `proof-source-stories-from-real-history-not-handpicked` — data pipeline, not marketing
- `proof-localise-social-proof-to-visitor-area` — psychological distance reduction
- `proof-surface-mixed-reviews-not-only-five-star` — blemishing effect
7. Personalised Conversion Triggers (MEDIUM-HIGH)
- `convert-trigger-paywall-on-specific-listings` — specific object beats generic modal
- `convert-use-loss-aversion-framing-on-soft-locks` — "don't lose what you built" (Kahneman)
- `convert-anchor-price-against-local-alternative` — role-appropriate local anchor
- `convert-never-interrupt-active-search` — natural pause points only (Fogg)
- `convert-re-engage-non-converting-registrants-personalised` — personalised triggers beat generic
8. Onboarding Intent Capture (MEDIUM)
- `onboard-ask-role-before-anything-else` — role drives branching
- `onboard-ask-highest-information-gain-first` — information gain ordering
- `onboard-prefill-from-inferred-signal` — confirmation beats data entry
- `onboard-make-optional-questions-genuinely-skippable` — no dark-pattern required markers
- `onboard-allow-answer-revision-without-restart` — revision without losing progress
9. Identity Stitching (MEDIUM)
- `stitch-preserve-profile-across-registration` — no reset at signup
- `stitch-use-deterministic-matching-for-returning-visitors` — email hash beats fingerprinting
- `stitch-avoid-cross-contamination-on-account-switch` — household hygiene
- `stitch-handle-multi-device-via-privacy-safe-signal` — deterministic-only cross-device
- `stitch-degrade-gracefully-on-low-confidence` — fresh beats bad merge
10. Pre-Member Measurement and Experimentation (MEDIUM)
- `measure-define-anonymous-to-member-as-primary-outcome` — one primary metric, rest are diagnostics
- `measure-attribute-conversion-to-signal-change` — profile-diff attribution
- `measure-segment-by-channel-and-visitor-profile` — Simpson's paradox prevention
- `measure-run-interleaving-for-fast-experiments` — 10-100x less sample for ranking
Living Context
This skill treats the product as evolving. Three living artefacts carry context across sessions, releases and team changes:
- `gotchas.md` — append-only diagnostic lessons from pre-member conversion incidents
- Visitor-concern matrix — the side-by-side table of what each side needs to validate, extended as new concerns surface
- Pre-member experiment log — every conversion experiment with hypothesis, cohort, intervention, outcome
Update all three after every shipped change.
How to Use
- Read `references/_sections.md` for category structure and cascade rationale
- Read `gotchas.md` for accumulated lessons before suggesting interventions
- Read individual rule files when a specific task matches the rule title
- Use `assets/templates/_template.md` to author new rules as the skill grows
Related Skills
- `marketplace-search-recsys-planning` — post-member retrieval planning (search, OpenSearch, ranking). Hand off after paid-member activation.
- `marketplace-personalisation` — post-member personalisation (AWS Personalize, impression tracking, feedback loops, two-sided matching). Hand off after paid-member activation.
Reference Files
| File | Description |
|---|---|
| references/_sections.md | Category definitions and cascade rationale |
| gotchas.md | Accumulated pre-member diagnostic lessons |
| assets/templates/_template.md | Template for authoring new rules |
| metadata.json | Version, discipline, research references |
Two-Sided Pre-Member Personalisation
Version 0.1.0 Marketplace Engineering April 2026
Note:
This document is mainly for agents and LLMs to follow when maintaining,
generating, or refactoring codebases. Humans may also find it useful,
but guidance here is optimized for automation and consistency by AI-assisted workflows.
---
Abstract
Comprehensive design and diagnostic guide for the pre-member journey of a two-sided trust marketplace. Covers anonymous signal inference, side-specific validation (what pet owners and pet sitters each need to see and believe before paying), information-asymmetry closure, progressive profile building, social proof, conversion psychology, onboarding intent capture, identity stitching and pre-member measurement. 53 rules across 10 categories, every rule grounded in published consumer-trust and decision research — Cialdini, Kahneman, Roth, Fogg, Bandura, Slovic, Nielsen Norman Group, and two-sided marketplace engineering literature. Functions as the precursor to the companion marketplace-personalisation and marketplace-search-recsys-planning skills; hand off at the paid-member boundary.
---
Table of Contents
1. Anonymous Signal Inference — CRITICAL
- 1.1 Capture Entry-Point Metadata on Every Page Load — CRITICAL (enables acquisition-channel attribution and personalisation)
- 1.2 Classify Inbound Intent from the Acquisition Channel — CRITICAL (enables channel-specific priors without interaction data)
- 1.3 Extract Role from URL Path and Referrer Before First Render — CRITICAL (enables side-specific content on the first page)
- 1.4 Infer Geography from IP with Confidence Caveats — CRITICAL (enables same-region content without false certainty)
- 1.5 Mint Anonymous Session Tokens from the First Request — CRITICAL (enables session-level personalisation without login)
- 1.6 Store Raw Signal Separately from Derived Features — CRITICAL (enables re-derivation when feature logic changes)
2. Pet Owner Validation and Trust — CRITICAL
- 2.1 Anchor Membership Cost Against the Visitor's Local Kennel Alternative — CRITICAL (enables saving-frame perception via local anchor)
- 2.2 Demystify Owner Effort Explicitly Before Payment — CRITICAL (reduces cognitive-load conversion loss)
- 2.3 Display Honest Local Availability, Not Inflated Global Counts — CRITICAL (prevents post-payment expectancy violation)
- 2.4 Rank Sitters by Experience with the Visitor's Pet Type — CRITICAL (prevents feasibility-mismatch objection)
- 2.5 Show Specific Local Owner Reviews, Not Global Averages — CRITICAL (enables identifiable-victim social proof)
- 2.6 Surface Safety Guarantees and Insurance Before Listings — CRITICAL (reduces risk-overweighting on rare bad outcomes)
3. Pet Sitter Validation and Opportunity — HIGH
- 3.1 Be Honest About First-Stay Competition for New Sitters — HIGH (prevents first-year churn from expectation violation)
- 3.2 Disclose Hidden Costs Transparently Before Payment — HIGH (prevents first-stay cost-shock churn)
- 3.3 Provide a Concrete First-Stay Path, Not Abstract Encouragement — HIGH (enables self-efficacy on the cold-start problem)
- 3.4 Rank Stays by the Sitter's Travel Goal, Not Just Supply Density — HIGH (prevents mismatch between inventory and actual desire)
- 3.5 Show Stay Inventory in the Sitter's Target Destination — HIGH (prevents generic-inventory disappointment)
- 3.6 Show Typical Daily Commitment per Stay, Not Vague Descriptions — HIGH (enables accurate effort-to-benefit calculation)
4. Information-Asymmetry Closure — HIGH
- 4.1 Display Acceptance Rate for the Visitor's Profile Shape — HIGH (prevents unrealistic expectations on rare-profile visitors)
- 4.2 Link to a Realistic First-Experience Story from a Peer — HIGH (enables narrative-driven expectation setting)
- 4.3 Route Unworkable Segments to Alternatives, Not to Payment — HIGH (prevents converting visitors who will churn)
- 4.4 Surface Lead-Time Reality for the Visitor's Dates — HIGH (prevents unmatchable-dates disappointment)
- 4.5 Surface Seasonal Supply Constraints Before Payment — HIGH (prevents seasonal-expectation mismatch)
- 4.6 Warn About the Cold-Start Penalty on Both Sides Pre-Payment — HIGH (prevents first-year churn from the cold-start surprise)
5. Progressive Profile Building — MEDIUM-HIGH
- 5.1 Build Profile Features Incrementally on Each Interaction — MEDIUM-HIGH (enables in-session preference learning without login)
- 5.2 Decay Profile Features with Session Inactivity — MEDIUM-HIGH (prevents stale clicks dominating the profile)
- 5.3 Persist Anonymous Profile Across Tabs and Reloads — MEDIUM-HIGH (prevents profile reset on page refresh)
- 5.4 Reset Profile Features on Explicit Role Changes — MEDIUM-HIGH (prevents cross-contamination between sitter and owner profiles)
- 5.5 Surface Profile Confidence Alongside Predictions — MEDIUM-HIGH (enables downstream decisions to respect uncertainty)
6. Social Proof and Lookalike Cohorts — MEDIUM-HIGH
- 6.1 Localise Social Proof to the Visitor's Geography — MEDIUM-HIGH (reduces psychological distance of proof)
- 6.2 Match Peer Stories to the Visitor's Inferred Cohort — MEDIUM-HIGH (enables similarity-driven persuasion)
- 6.3 Source Peer Stories from Real User History, Not Handpicked Marketing — MEDIUM-HIGH (prevents testimonial-skepticism collapse)
- 6.4 Surface Mixed-Positive Reviews, Not Only Five-Star — MEDIUM-HIGH (enables blemishing-effect credibility)
- 6.5 Use Specific Peer Stories at Decision Points, Not Aggregate Stats — MEDIUM-HIGH (enables specific-beats-aggregate social proof)
7. Personalised Conversion Triggers — MEDIUM-HIGH
- 7.1 Anchor Membership Price Against the Visitor's Most Local Alternative — MEDIUM-HIGH (enables saving-frame perception via local anchor)
- 7.2 Never Interrupt an Active Search with a Conversion Modal — MEDIUM-HIGH (prevents task-flow rejection)
- 7.3 Reengage Non-Converting Registrants with Personalised Triggers — MEDIUM-HIGH (enables targeted reactivation of registered-not-converted cohort)
- 7.4 Trigger the Paywall on Specific Listings, Not Generic Upgrade Prompts — MEDIUM-HIGH (enables cognitive-ease conversion)
- 7.5 Use Loss-Aversion Framing on Soft-Locked Content — MEDIUM-HIGH (2-3x stronger than equivalent gain framing)
8. Onboarding Intent Capture — MEDIUM
- 8.1 Allow Answer Revision Without Restart — MEDIUM (prevents mid-form abandonment on realisation)
- 8.2 Ask Role Before Any Other Onboarding Question — MEDIUM (enables role-branched onboarding from the first question)
- 8.3 Ask the Highest-Information-Gain Question Earliest — MEDIUM (reduces drop-off per unit of signal captured)
- 8.4 Make Optional Questions Genuinely Skippable — MEDIUM (reduces form-abandonment drop-off)
- 8.5 Prefill Onboarding Answers from Inferred Signal — MEDIUM (reduces friction by removing redundant typing)
9. Identity Stitching — MEDIUM
- 9.1 Avoid Cross-Contamination When Users Switch Accounts — MEDIUM (prevents household-device profile merging)
- 9.2 Degrade Gracefully When Stitching Confidence Is Low — MEDIUM (prevents bad merges worse than no merges)
- 9.3 Handle Multi-Device Visitors via Privacy-Safe Deterministic Signals — MEDIUM (enables cross-device profile continuity without fingerprinting)
- 9.4 Preserve Inferred Profile Across the Registration Transition — MEDIUM (prevents personalisation reset at signup)
- 9.5 Use Deterministic Matching for Returning Visitors — MEDIUM (prevents incorrect profile merges)
10. Pre-Member Measurement and Experimentation — MEDIUM
- 10.1 Attribute Conversion to the Signal That Changed the Profile — MEDIUM (enables intervention-level conversion attribution)
- 10.2 Define Anonymous-to-Member Conversion as the Primary Outcome — MEDIUM (prevents proxy-metric optimisation)
- 10.3 Run Interleaving for Fast Pre-Member Experiments — MEDIUM (reduces required sample size by 10-100x)
- 10.4 Segment Conversion Measurement by Channel and Visitor Profile — MEDIUM (prevents aggregate-masked segment regressions)
---
References
1. https://www.influenceatwork.com/principles-of-persuasion/ 2. https://www.jstor.org/stable/1914185 3. https://www.hup.harvard.edu/books/9780544291133 4. https://bjfogg.com/fbm_files/page4_1.pdf 5. https://psycnet.apa.org/doi/10.1037/0033-295X.84.2.191 6. https://www.decisionresearch.org/wp-content/uploads/2017/06/rd6501.pdf 7. https://www.nngroup.com/articles/trustworthiness/ 8. https://www.nngroup.com/articles/required-fields/ 9. https://www.nngroup.com/articles/progressive-disclosure/ 10. https://psycnet.apa.org/doi/10.1037/a0018963 11. https://link.springer.com/article/10.1023/A:1022299422219 12. https://psycnet.apa.org/doi/10.1037/0022-3514.79.5.701 13. https://academic.oup.com/jcr/article-abstract/38/5/846/1791985 14. https://experimentguide.com/ 15. https://dl.acm.org/doi/10.1145/2433396.2433429 16. https://www.lukew.com/resources/web_form_design.asp 17. https://auth0.com/docs/manage-users/user-accounts/user-profiles/progressive-profiling 18. https://docs.mixpanel.com/docs/tracking-methods/id-management/identifying-users-simplified 19. https://docs.snowplow.io/docs/modeling-your-data/modeling-your-data-with-dbt/package-features/identity-stitching/ 20. https://docs.treasuredata.com/products/customer-data-platform/real-time/real-time-id-stitching-overview 21. https://www.kameleoon.com/blog/contextual-bandits 22. https://www.optimizely.com/insights/blog/contextual-bandits-in-personalization/ 23. https://dl.acm.org/doi/10.1145/2645710.2645732 24. https://www.kdd.org/kdd2018/accepted-papers/view/real-time-personalization-using-embeddings-for-search-ranking-at-airbnb 25. https://medium.com/airbnb-engineering/learning-market-dynamics-for-optimal-pricing-97cffbcc53e3 26. https://developers.google.com/machine-learning/guides/rules-of-ml 27. https://www.edelman.com/trust-barometer 28. https://www.tandfonline.com/doi/abs/10.1080/08934219309367485
---
Source Files
This document was compiled from individual reference files. For detailed editing or extension:
| File | Description |
|---|---|
| references/_sections.md | Category definitions and impact ordering |
| assets/templates/_template.md | Template for creating new rules |
| SKILL.md | Quick reference entry point |
| metadata.json | Version and reference URLs |
{Action-Oriented Rule Title in Title Case}
{One-to-three sentences explaining WHY this rule matters. Ground the explanation in primary research where possible — cite the author and the specific claim. This is the most important part of the rule; LLMs generalise better from understood reasoning than from dictation. Do not say "always do X" — explain what the research shows, what goes wrong when the rule is not followed, and how the visitor's specific psychological or market-level situation produces the downstream effect.}
Incorrect ({specific failure mode in 3-6 words}):
```{language} // Production-realistic bad code — not a strawman. // Keep it under 20 lines. Use realistic names: seeker, provider, // visitor, anon_session, request_id, trust_score, kennel_rate — // never foo, bar, MyComponent, doSomething.
**Correct ({specific benefit in 3-6 words}):**
// Production-realistic good code that differs minimally from the incorrect version — // only the key insight changes. Same variable names, same structure, different // behaviour where it matters. Under 20 lines.
Reference: [{Primary Research Source}](https://url-to-paper-or-book-or-engineering-blog)
## Authoring Checklist
Before saving, verify:
- [ ] Frontmatter has `title`, `impact`, `impactDescription`, `tags`
- [ ] First tag matches the category prefix from `_sections.md`
- [ ] Impact description uses a pattern verb (enables, reduces, prevents, avoids, saves) or quantifies
- [ ] The H2 title matches the frontmatter title exactly (no hyphens in the first word — breaks the imperative regex)
- [ ] Both code blocks have a language specifier (```typescript, ```python, ```json)
- [ ] Both annotations `(failure mode)` and `(benefit)` are specific, not `(bad)` or `(good)`
- [ ] No vague language: `might`, `perhaps`, `maybe`, `it is recommended`
- [ ] No marketing language: `powerful`, `magic`, `seamless`, `blazing fast`
- [ ] No generic names: `foo`, `bar`, `MyComponent`, `doSomething`, `processData`
- [ ] The explanation cites primary research or a foundational text where the claim is established
- [ ] Rule validates with `node scripts/validate-skill.js {skill-dir}` with no new errors
Gotchas
Living log of diagnostic lessons accumulated from pre-member conversion incidents. Every entry is dated, describes a concrete surprise or failure mode, and records the resolution. Append new entries at the top. Never delete entries — older lessons still carry context even when the specific code they refer to has changed.
Format
### Short descriptive title of the surprise
Date: YYYY-MM-DD
Context: what the team was working on
Symptom: what the observable failure mode was
Root cause: what was actually wrong
Resolution: how the team fixed it
Lesson: the generalisable takeaway---
Example seed: geo-IP fallback trapped Spanish visitors in an English experience
Date: 2026-04-12 Context: Worked example illustrating the gotchas convention — replace when a real one is captured. Symptom: Conversion rate from Spanish-speaking anonymous traffic dropped 30% over a weekend. Root cause: A geo-IP library upgrade lowered the confidence threshold for country detection. Spanish visitors routed via Amazon CDN edge in Dublin were being classified as Irish with high confidence, served English-only content, and bouncing. Resolution: Reverted the confidence-threshold change, added an explicit "is this your country?" chooser when the language of the Accept-Language header contradicts the geo-IP result. Lesson: Geo-IP confidence alone is not enough — cross-check with Accept-Language, and surface a correction UI when signals disagree. Related rule: signal-infer-geography-with-confidence.
{
"version": "1.0.4",
"organization": "Marketplace Engineering",
"technology": "Two-Sided Pre-Member Personalisation",
"discipline": "distillation",
"type": "library-reference",
"date": "April 2026",
"abstract": "Comprehensive design and diagnostic guide for the pre-member journey of a two-sided trust marketplace. Covers anonymous signal inference, side-specific validation (what pet owners and pet sitters each need to see and believe before paying), information-asymmetry closure, progressive profile building, social proof, conversion psychology, onboarding intent capture, identity stitching and pre-member measurement. 53 rules across 10 categories, every rule grounded in published consumer-trust and decision research — Cialdini, Kahneman, Roth, Fogg, Bandura, Slovic, Nielsen Norman Group, and two-sided marketplace engineering literature. Functions as the precursor to the companion marketplace-personalisation and marketplace-search-recsys-planning skills; hand off at the paid-member boundary.",
"references": [
"https://www.influenceatwork.com/principles-of-persuasion/",
"https://www.jstor.org/stable/1914185",
"https://www.hup.harvard.edu/books/9780544291133",
"https://bjfogg.com/fbm_files/page4_1.pdf",
"https://psycnet.apa.org/doi/10.1037/0033-295X.84.2.191",
"https://www.decisionresearch.org/wp-content/uploads/2017/06/rd6501.pdf",
"https://www.nngroup.com/articles/trustworthiness/",
"https://www.nngroup.com/articles/required-fields/",
"https://www.nngroup.com/articles/progressive-disclosure/",
"https://psycnet.apa.org/doi/10.1037/a0018963",
"https://link.springer.com/article/10.1023/A:1022299422219",
"https://psycnet.apa.org/doi/10.1037/0022-3514.79.5.701",
"https://academic.oup.com/jcr/article-abstract/38/5/846/1791985",
"https://experimentguide.com/",
"https://dl.acm.org/doi/10.1145/2433396.2433429",
"https://www.lukew.com/resources/web_form_design.asp",
"https://auth0.com/docs/manage-users/user-accounts/user-profiles/progressive-profiling",
"https://docs.mixpanel.com/docs/tracking-methods/id-management/identifying-users-simplified",
"https://docs.snowplow.io/docs/modeling-your-data/modeling-your-data-with-dbt/package-features/identity-stitching/",
"https://docs.treasuredata.com/products/customer-data-platform/real-time/real-time-id-stitching-overview",
"https://www.kameleoon.com/blog/contextual-bandits",
"https://www.optimizely.com/insights/blog/contextual-bandits-in-personalization/",
"https://dl.acm.org/doi/10.1145/2645710.2645732",
"https://www.kdd.org/kdd2018/accepted-papers/view/real-time-personalization-using-embeddings-for-search-ranking-at-airbnb",
"https://medium.com/airbnb-engineering/learning-market-dynamics-for-optimal-pricing-97cffbcc53e3",
"https://developers.google.com/machine-learning/guides/rules-of-ml",
"https://www.edelman.com/trust-barometer",
"https://www.tandfonline.com/doi/abs/10.1080/08934219309367485"
]
}
Marketplace Pre-Member Personalisation Skill
Research-grounded best-practices skill for the pre-member journey of a two-sided trust marketplace — from anonymous landing through onboarding, registration, and the paid- membership paywall.
Overview
This skill is a distillation of published consumer-trust and decision research applied to the specific problem of converting anonymous visitors into paid members of a two- sided trust marketplace. It contains 53 rules across 10 categories, every rule cited back to primary research (Cialdini, Kahneman, Roth, Fogg, Bandura, Slovic, Nielsen Norman Group) or to production engineering literature (Airbnb, DoorDash).
The skill is the precursor to marketplace-personalisation and marketplace-search-recsys-planning — it handles everything before the paid-member boundary, and explicitly hands off to those skills at the moment of conversion.
Structure
marketplace-pre-member-personalisation/
├── SKILL.md # Entry point, category index, research foundations
├── AGENTS.md # Compiled navigation (built by script)
├── metadata.json # Version, discipline, research references
├── README.md # This file
├── gotchas.md # Living diagnostic lessons
├── references/
│ ├── _sections.md # Category definitions and cascade rationale
│ ├── signal-*.md # Anonymous Signal Inference (6 rules)
│ ├── owner-*.md # Pet Owner Validation and Trust (6 rules)
│ ├── sitter-*.md # Pet Sitter Validation and Opportunity (6 rules)
│ ├── gap-*.md # Information-Asymmetry Closure (6 rules)
│ ├── profile-*.md # Progressive Profile Building (5 rules)
│ ├── proof-*.md # Social Proof and Lookalike Cohorts (5 rules)
│ ├── convert-*.md # Personalised Conversion Triggers (5 rules)
│ ├── onboard-*.md # Onboarding Intent Capture (5 rules)
│ ├── stitch-*.md # Identity Stitching (5 rules)
│ └── measure-*.md # Pre-Member Measurement and Experimentation (4 rules)
└── assets/
└── templates/
└── _template.md # Template for authoring new rulesGetting Started
From the repo root, install plugin dependencies and run the skill validator:
pnpm install
pnpm build
pnpm validateThe validator runs structural and substance checks against the skill:
node scripts/validate-skill.js skills/.experimental/marketplace-pre-member-personalisationBuild the compiled navigation document:
node scripts/build-agents-md.js skills/.experimental/marketplace-pre-member-personalisationCreating a New Rule
Rules go in references/ with a filename of the form {prefix}-{slug}.md, where {prefix} matches an existing category in references/_sections.md. Copy assets/templates/_template.md as a starting point and fill in frontmatter and body.
A rule must include:
- YAML frontmatter:
title,impact,impactDescription,tags(first tag is the prefix) - One-to-three sentence explanation of why the rule matters, grounded in primary research where possible
- An
**Incorrect (specific description):**code block with production-realistic code - A
**Correct (specific description):**code block that differs minimally from the incorrect - A
Reference:line linking to a research paper, book, or production engineering blog
Run pnpm validate after adding or editing rules.
Rule File Structure
Each rule has a strict structure enforced by the validator:
---
title: Show Specific Local Owner Reviews, Not Global Averages
impact: CRITICAL
impactDescription: enables identifiable-victim social proof
tags: owner, social-proof, reviews
---
## Show Specific Local Owner Reviews, Not Global Averages
Explanation paragraph — why the rule matters, what research supports it,
and how it closes a specific objection a visitor has before paying.
**Incorrect (concrete failure mode):**
\`\`\`typescript
// Production-realistic bad example
\`\`\`
**Correct (concrete solution):**
\`\`\`typescript
// Production-realistic good example
\`\`\`
Reference: [Primary Research Source](https://example.com/paper)File Naming Convention
- Skill directory: kebab-case matching the skill name (
marketplace-pre-member-personalisation) - Rule files:
{category-prefix}-{slug}.mdwith kebab-case slugs - Templates:
assets/templates/_template.md(underscore prefix excludes from rule listings) - Category prefixes are 3-8 lowercase letters and defined once in
_sections.md
Impact Levels
Categories and rules use six impact levels ordered from highest to lowest cascade impact:
| Level | Meaning | Cascade Effect |
|---|---|---|
CRITICAL | Affects every downstream stage | Everything waits on this |
HIGH | Affects most downstream stages | Major path is blocked |
MEDIUM-HIGH | Affects specific downstream paths | Partial blocking |
MEDIUM | Local impact with high frequency | Common but contained |
LOW-MEDIUM | Micro-impact in hot paths | Measurable in loops |
LOW | Edge cases and expert patterns | Specific scenarios only |
Target distribution for a 40-60 rule distillation: 1-2 CRITICAL categories, 2-3 HIGH, the rest MEDIUM or lower. This skill has 2 CRITICAL, 2 HIGH, 3 MEDIUM-HIGH and 3 MEDIUM categories.
Scripts
The dev-skill plugin provides two scripts used by this skill:
scripts/validate-skill.js— runs structural and substance validation (required before shipping)scripts/build-agents-md.js— compiles the navigation document (never write AGENTS.md manually)
Example invocations:
node scripts/validate-skill.js skills/.experimental/marketplace-pre-member-personalisation
node scripts/validate-skill.js skills/.experimental/marketplace-pre-member-personalisation --sections-only
node scripts/build-agents-md.js skills/.experimental/marketplace-pre-member-personalisationContributing
- New rules must follow the structure above and pass
pnpm validatewith zero errors - Every rule must have incorrect and correct examples with specific annotations
- Every rule must cite primary research (peer-reviewed paper, foundational book, or production engineering blog with data)
- Avoid hedging language (
might,perhaps,it is recommended) — use imperative form - Quantify impact where possible or use pattern verbs (
enables,prevents,reduces) - Update
gotchas.mdwhen a new diagnostic lesson is learned - Never edit
AGENTS.mdmanually — it is regenerated bybuild-agents-md.js
Sections
This file defines all sections, their ordering, impact levels, and descriptions. The section ID (in parentheses) is the filename prefix used to group rules.
Categories are ordered by cascade impact on the pre-member conversion journey. The central insight driving the ordering is that both sides of a trust marketplace are trying to validate specific beliefs before paying, and the research on consumer trust and two-sided markets is clear that those beliefs are side-specific. Signal inference must come first because nothing downstream can be side-aware without it. Owner and sitter validation come next because they encode what each side actually needs to see. Everything else is the supporting infrastructure — progressive profiling, gap closure, social proof, conversion psychology, onboarding capture, identity stitching and measurement.
---
1. Anonymous Signal Inference (signal)
Impact: CRITICAL Description: Without extracting role, intent, geography and urgency from URL path, referrer, query params, geo-IP and device within the first request, no downstream personalisation layer can be side-aware and every visitor is treated as a blank slate.
2. Pet Owner Validation and Trust (owner)
Impact: CRITICAL Description: Pet owners have six specific validations they perform before paying — safety, competence, availability, matching feasibility, value-versus-kennels and effort — and the personalisation layer must surface evidence for each one concretely because aggregate reassurances fail against loss-aversion and risk-overweighting.
3. Pet Sitter Validation and Opportunity (sitter)
Impact: HIGH Description: Pet sitters need to see concrete opportunity in their target destinations, honest first-stay competition, a credible path to acceptance, realistic daily commitment and transparent hidden costs before paying — because the single biggest cause of first-year sitter churn is discovering these facts after payment.
4. Information-Asymmetry Closure (gap)
Impact: HIGH Description: Both sides are missing information they do not know they are missing — cold-start penalty on the first transaction, seasonal supply collapse, acceptance rates for their profile shape, lead time distributions — and surfacing these facts honestly before conversion trades a small conversion cost for a large retention gain.
5. Progressive Profile Building (profile)
Impact: MEDIUM-HIGH Description: A visitor who clicks, scrolls and dwells reveals preferences with every interaction that never return to the system unless the session accumulates them into an evolving profile persisted across tabs, reloads and the anonymous-to-registered transition.
6. Social Proof and Lookalike Cohorts (proof)
Impact: MEDIUM-HIGH Description: Cialdini's research shows specific social proof converts dramatically better than aggregate, and the identifiable-victim effect confirms named-person evidence beats statistics — so pre-member proof must be localised, cohort-matched and sourced from real-user history rather than handpicked marketing testimonials.
7. Personalised Conversion Triggers (convert)
Impact: MEDIUM-HIGH Description: The paywall moment is governed by loss aversion, price anchoring and cognitive ease — three well-documented psychological mechanisms that together determine whether the visitor pays or bounces, and each demands a different personalised treatment than a generic upgrade modal.
8. Onboarding Intent Capture (onboard)
Impact: MEDIUM Description: Every onboarding question costs friction measured in drop-off, and must earn its place by producing downstream personalisation lift — so question ordering follows information gain, optional questions are genuinely skippable, and inferred answers are pre-filled wherever the system can credibly guess.
9. Identity Stitching (stitch)
Impact: MEDIUM Description: The transition from anonymous session to registered account to paid member must preserve every inferred feature the system has built, using deterministic matching where identifiers exist and degrading gracefully where they do not, because bad stitching is worse than no stitching.
10. Pre-Member Measurement and Experimentation (measure)
Impact: MEDIUM Description: Anonymous-to-member conversion is the only primary outcome that matters pre-member, and attribution, segmentation and experiment velocity determine whether the team can actually learn from the traffic they have.
Anchor Membership Price Against the Visitor's Most Local Alternative
Kahneman's price-anchoring research shows that any number evaluated in isolation is compared implicitly to whatever reference the user's mind first produces, and the best anchors are those the visitor can concretely calculate against their own life. For pet owners, the dominant mental reference is the local kennel. For pet sitters, it is the local hotel or Airbnb. Showing membership price against a local, concrete alternative that the visitor already knows and prices regularly reframes the membership from "an expense" to "a saving", without any change to the actual price.
Incorrect (isolated price with no anchor):
function PriceLabel() {
return <span className="price">£129/year</span>
}Correct (price anchored against the visitor's role-appropriate local alternative):
async function PriceLabel({ visitorRole, visitorRegion }: Props) {
if (visitorRole === "owner") {
const kennel = await rates.localKennelNightly(visitorRegion)
return (
<div>
<span className="price">£129/year</span>
<span className="anchor">
About {Math.round(129 / kennel)} nights at a local kennel
(£{kennel}/night in {visitorRegion})
</span>
</div>
)
}
if (visitorRole === "sitter") {
const airbnb = await rates.localAirbnbNightly(visitorRegion)
return (
<div>
<span className="price">£129/year</span>
<span className="anchor">
About {Math.round(129 / airbnb)} nights in a budget Airbnb in{" "}
{visitorRegion} (£{airbnb}/night). Members typically save 30+ nights a year.
</span>
</div>
)
}
return <span className="price">£129/year</span>
}Reference: Kahneman and Tversky — Prospect Theory: An Analysis of Decision under Risk (Econometrica 1979)
Never Interrupt an Active Search with a Conversion Modal
The Fogg Behavior Model is clear that triggers fired while the user is in the middle of a task flow produce rejection because the trigger competes with the task for attention and usually loses. A visitor actively browsing listings is executing a task (evaluating candidates) and an unrelated upgrade modal is exactly the kind of interrupter that gets dismissed reflexively. The right paywall timing is at natural pause points — when the visitor finishes scrolling a result page, when they click a specific listing that requires membership to proceed, when they return in a second session — not when they are mid-scroll or mid-comparison.
Incorrect (time-based modal interrupts active browsing):
function SearchPage() {
useEffect(() => {
const timer = setTimeout(() => showUpgradeModal(), 30_000)
return () => clearTimeout(timer)
}, [])
return <Listings />
}Correct (paywall triggered at natural pause points):
function SearchPage() {
const [showPaywall, setShowPaywall] = useState(false)
useEffect(() => {
const onScrollPastEnd = () => setShowPaywall(true)
window.addEventListener("scroll_past_end", onScrollPastEnd)
return () => window.removeEventListener("scroll_past_end", onScrollPastEnd)
}, [])
function onListingAction(listing: Listing, action: "message" | "apply") {
if (!currentUser.isMember) {
setShowPaywall(true, { triggeringListing: listing, triggeringAction: action })
} else {
performAction(listing, action)
}
}
return (
<>
<Listings onListingAction={onListingAction} />
{showPaywall && <PaywallModal />}
</>
)
}Reengage Non-Converting Registrants with Personalised Triggers
A registered-but-not-paid visitor is a highly valuable cohort — they converted through the highest-friction step (account creation) and then stopped. Research on lifecycle re-engagement (Tellis et al on persistence of advertising effects, and the broader CRM literature) shows that the difference between a personalised re-engagement trigger and a generic reminder is an order of magnitude in reactivation. Specifically: a visitor who searched Barcelona three times and bookmarked two sitters should receive a Barcelona-specific email referencing those specific sitters, not a generic "complete your signup" template. The personalisation layer owns this — it already knows the search history — and it must ship the signal to the CRM system.
Incorrect (generic abandoned-signup reminder to every registered non-member):
def send_reminder(registrant: Registrant) -> None:
email.send(
to=registrant.email,
template="complete_signup_reminder",
params={"first_name": registrant.first_name},
)Correct (personalised trigger referencing the visitor's actual search history):
def send_reminder(registrant: Registrant) -> None:
profile = profile_store.get(registrant.anon_session)
if not profile:
email.send(to=registrant.email, template="generic_welcome_back")
return
top_search = profile.top_search_destination()
bookmarked = profile.bookmarked_listings()
if top_search and bookmarked:
email.send(
to=registrant.email,
template="saved_listings_in_destination",
params={
"first_name": registrant.first_name,
"destination": top_search,
"listings": [l.to_email_card() for l in bookmarked[:3]],
"local_kennel_rate": rates.local_kennel_nightly(top_search),
},
)
elif top_search:
email.send(
to=registrant.email,
template="destination_availability_update",
params={
"first_name": registrant.first_name,
"destination": top_search,
"new_stays_count": listings.new_in_destination(top_search, days=7),
},
)
else:
email.send(to=registrant.email, template="generic_welcome_back")Reference: Tellis — Effective Frequency in Advertising (Journal of Advertising Research 1997)
Trigger the Paywall on Specific Listings, Not Generic Upgrade Prompts
The Fogg Behavior Model identifies a trigger as the third leg of any behaviour change, alongside motivation and ability. Generic upgrade triggers ("become a member to unlock features") produce weaker conversion than specific triggers attached to an object the visitor is actively evaluating ("to message Sarah, become a member"). The specific trigger has higher motivation because it references a concrete thing the visitor has already chosen to care about, and lower ability cost because the next step is obvious. Pair the paywall trigger to the specific listing or action the visitor attempted, not to a homepage upgrade CTA.
Incorrect (generic upgrade modal disconnected from what the visitor clicked):
function PaywallModal({ isOpen, onClose }: Props) {
return (
<Modal open={isOpen} onClose={onClose}>
<h2>Upgrade to membership</h2>
<p>Unlock all platform features for £129/year.</p>
<ul>
<li>Message any sitter</li>
<li>Apply to any stay</li>
<li>See verified profiles</li>
</ul>
<button>Join now</button>
</Modal>
)
}Correct (paywall trigger references the specific listing the visitor clicked):
function PaywallModal({ listing, triggeringAction }: Props) {
return (
<Modal open>
<ListingCard listing={listing} />
<h2>
To {triggeringAction === "message" ? "message" : "apply to"}{" "}
{listing.sitterFirstName}, become a member
</h2>
<p>
{listing.sitterFirstName} has completed {listing.sitter.completedStays} stays
and typically replies within {listing.sitter.avgResponseHours} hours.
</p>
<p>
Membership is £129/year — about {Math.round(129 / kennelRate)} nights at a
local kennel.
</p>
<button>Join to message {listing.sitterFirstName}</button>
</Modal>
)
}Use Loss-Aversion Framing on Soft-Locked Content
Kahneman and Tversky's prospect theory showed that losses are psychologically weighted roughly 2-3× gains of the same magnitude, and decades of replication confirm the effect holds across domains. For pre-member conversion, this means that framing the paywall as "you will lose access to the sitters you bookmarked" converts materially better than "upgrade to save your bookmarks". The visitor must have first invested something to lose — bookmarks, saved searches, draft messages — which is why the soft-lock pattern (let the visitor accumulate state, then reference the loss when they hit the paywall) outperforms an immediate hard wall.
Incorrect (generic gain framing with nothing for the visitor to lose):
function UpgradePrompt() {
return (
<div>
<h3>Upgrade for more features</h3>
<p>Save listings, contact sitters, and apply to stays.</p>
<button>Upgrade</button>
</div>
)
}Correct (loss framing references specific accumulated state):
async function UpgradePrompt({ visitorId }: { visitorId: string }) {
const bookmarked = await savedListings.count(visitorId)
const drafted = await drafts.count(visitorId)
if (bookmarked === 0 && drafted === 0) {
return <SoftCTA>Browse more listings to save your favourites</SoftCTA>
}
return (
<div>
<h3>Don't lose what you've built</h3>
<p>
You have{" "}
{bookmarked > 0 && <strong>{bookmarked} saved sitters</strong>}
{bookmarked > 0 && drafted > 0 && " and "}
{drafted > 0 && <strong>{drafted} draft messages</strong>}
{" "}waiting for you. Join now to keep them.
</p>
<button>Save my progress and join</button>
</div>
)
}Reference: Kahneman and Tversky — Prospect Theory: An Analysis of Decision under Risk (Econometrica 1979)
Display Acceptance Rate for the Visitor's Profile Shape
Acceptance rates on a trust marketplace are not uniform across profile shapes. An owner with a low-maintenance cat and flexible dates has a dramatically higher sitter acceptance rate than an owner with an elderly diabetic Great Dane that needs insulin twice daily and a fenced garden. An established sitter with ten reviews has a dramatically higher owner acceptance rate than a new sitter with none. Showing the visitor their cohort's acceptance rate pre-payment lets them set correct expectations and either adjust their profile (relax a constraint, add credentials) or accept the reality — both of which are better outcomes than paying and being blindsided.
Incorrect (no acceptance-rate context shown before membership commitment):
def membership_cta(visitor: AnonVisitor) -> CallToAction:
return CallToAction(
headline="Join now and start sitting",
cta_label="Become a member",
price="£129/year",
)Correct (cohort-specific acceptance rate with adjustment suggestions):
def membership_cta(visitor: AnonVisitor) -> CallToAction:
cohort = classify_visitor_cohort(visitor)
acceptance = analytics.acceptance_rate_for_cohort(cohort)
headline = f"Join now and start sitting"
caveat = None
suggestion = None
if acceptance.rate < 0.35:
caveat = (
f"Members with profiles like yours ({cohort.label}) have about a "
f"{int(acceptance.rate * 100)}% acceptance rate on typical applications."
)
suggestion = cohort.top_adjustment_to_improve_acceptance()
return CallToAction(
headline=headline,
caveat=caveat,
improvement_suggestion=suggestion,
cta_label="Become a member",
price="£129/year",
)Reference: Real-time Personalization using Embeddings for Search Ranking at Airbnb (KDD 2018)
Link to a Realistic First-Experience Story from a Peer
Narrative transportation research (Green and Brock 2000) shows that people absorb information from a first-person story more durably than from statistics or abstract claims, and the effect is strongest when the narrator shares visible characteristics with the listener. A first-time pet owner hesitating on payment will integrate "here is what my first stay was like, good and bad" from another first-time owner more deeply than any testimonial or statistic — especially when the story includes the friction and not just the triumph. Link the visitor to a cohort-matched real story from a member whose first experience happened recently, with honest detail about what was harder than expected as well as what worked.
Incorrect (polished success-only testimonial, disconnected from visitor):
function Testimonial() {
return (
<blockquote>
"Joining was the best decision I ever made. My pets are happy and I travel without worry!"
— Sarah T., Member since 2018
</blockquote>
)
}Correct (cohort-matched, recent, honest first-experience narrative):
async function PeerStory({ visitorCohort }: { visitorCohort: Cohort }) {
const story = await stories.pickRealFirstExperience({
matchingCohort: visitorCohort,
includeFriction: true,
maxAgeMonths: 6,
})
return (
<article>
<h3>
{story.author.firstName} in {story.author.city}, {story.monthsSinceJoined} months in
</h3>
<p>{story.summary}</p>
<section>
<h4>What worked</h4>
<p>{story.whatWorked}</p>
</section>
<section>
<h4>What was harder than I expected</h4>
<p>{story.whatWasHard}</p>
</section>
<section>
<h4>What I'd do differently</h4>
<p>{story.lessonLearned}</p>
</section>
</article>
)
}Route Unworkable Segments to Alternatives, Not to Payment
A new sitter trying to book a listing in central Lisbon for the first week of August is running a losing trade — the competition is saturated, the lead time is wrong, and the profile is weakest. Paying this visitor into membership is worse than sending them away, because they will churn within weeks and the platform will own both a refund request and a bad word-of-mouth review. The right move on unworkable segments is to route the visitor to an alternative that they can actually succeed in — a nearby city, an off-season month, a less-competitive cohort — even if it means declining their current intent. Lifetime-value economics favour this trade across every published marketplace model.
Incorrect (convert every visitor regardless of feasibility):
def on_search_intent(visitor: AnonVisitor, intent: SearchIntent) -> Response:
return Response(
paywall=True,
cta="Become a member to apply",
listing_preview=stays.search(intent)[:12],
)Correct (feasibility gate routes unworkable segments to alternatives):
def on_search_intent(visitor: AnonVisitor, intent: SearchIntent) -> Response:
feasibility = assess_feasibility(visitor=visitor, intent=intent)
if feasibility.score >= 0.4:
return Response(
paywall=True,
cta="Become a member to apply",
listing_preview=stays.search(intent)[:12],
)
return Response(
paywall=False,
headline="These dates and destination are very hard for new sitters",
reasoning=feasibility.reasons,
alternatives=[
{"label": alt.label, "description": alt.why_easier, "intent": alt.intent}
for alt in feasibility.alternatives[:3]
],
soft_cta="Try an alternative or keep looking",
)Reference: Alvin Roth — Who Gets What and Why: The New Economics of Matchmaking and Market Design
Surface Lead-Time Reality for the Visitor's Dates
Lead time is the single most-volatile variable in a two-sided trust marketplace — popular destinations in popular months are typically booked three to six months in advance, while off-season or rural stays can often be booked with two weeks notice. Visitors rarely know this when they land, and they plan trips or stays based on implicit assumptions that are wrong for their specific segment. A visitor searching Lisbon for dates three weeks out has effectively already failed, and the honest thing is to surface the lead-time reality for their dates before they pay, routing them either to flexible dates or to alternative destinations where their lead time is viable.
Incorrect (no lead-time context on date selection):
function DatePicker({ onConfirm }: Props) {
return (
<div>
<Calendar onSelect={onConfirm} />
<button>Search</button>
</div>
)
}Correct (lead-time warning with specific median booking advance):
async function DatePicker({ destination, onConfirm }: Props) {
const [dates, setDates] = useState<DateRange | null>(null)
const leadTime = dates
? await analytics.medianLeadTime({ destination, dateRange: dates })
: null
const daysUntilStart = dates ? daysBetween(new Date(), dates.start) : null
const isTight = daysUntilStart !== null && leadTime !== null && daysUntilStart < leadTime.medianDays
return (
<div>
<Calendar onSelect={setDates} />
{leadTime && dates && (
<p className={isTight ? "warning" : "muted"}>
{isTight
? `Bookings in ${destination} for ${dates.start.toLocaleDateString()} usually happen ${leadTime.medianDays} days in advance. You have ${daysUntilStart} days — supply will be thin.`
: `Typical booking lead time in ${destination}: ${leadTime.medianDays} days. You have time.`}
</p>
)}
<button onClick={() => dates && onConfirm(dates)}>Search</button>
</div>
)
}Reference: Alvin Roth — Who Gets What and Why: The New Economics of Matchmaking and Market Design
Surface Seasonal Supply Constraints Before Payment
Two-sided marketplaces have strong seasonal cycles that neither side of a first-time visitor tends to understand. Summer months concentrate demand in coastal and urban destinations while supply stays roughly flat, producing acceptance-rate collapses that surprise new users. Winter months in southern Europe often swing the other way. A visitor planning a July trip to the Amalfi Coast needs to know that supply is near-zero for their dates before paying, not after. Surfacing the seasonal pattern for the visitor's destination and dates honestly — with a chart, a sentence, or a soft-route to off-season alternatives — is one of the highest-leverage interventions available pre-payment.
Incorrect (static trust signal with no seasonal awareness):
function DestinationStats({ destination }: Props) {
return (
<div>
<h3>{destination}</h3>
<p>Over {totalStaysInDestination(destination)} stays available year-round</p>
</div>
)
}Correct (seasonal curve with the visitor's month highlighted):
async function DestinationStats({ destination, month }: Props) {
const curve = await analytics.seasonalSupplyCurve({ destination, months: 12 })
const visitorMonth = curve.find((c) => c.month === month)
const flag = visitorMonth && visitorMonth.supplyIndex < 0.3 ? "tight" : "ok"
return (
<div>
<h3>{destination} supply by month</h3>
<SparkChart data={curve} highlight={month} />
{flag === "tight" && (
<p className="warning">
{destination} in {month} is one of the tightest months of the year —{" "}
{Math.round(visitorMonth!.supplyIndex * 100)}% of annual average supply.
Consider {curve.filter((c) => c.supplyIndex > 0.8).map((c) => c.month).join(" or ")}.
</p>
)}
</div>
)
}Reference: Learning Market Dynamics for Optimal Pricing at Airbnb
Warn About the Cold-Start Penalty on Both Sides Pre-Payment
Matching-market research (Roth) and applied recommender research (Google Rules of ML) both show that the first transaction on a trust marketplace is disproportionately harder than any subsequent one — a new sitter has no reviews so owners filter them out, a new owner has no reviews so sitters judge them carefully. Visitors pay for membership expecting the typical member experience and discover only after payment that the first transaction is the hardest one they will ever do. This is a known, measurable pattern, and surfacing it honestly — with specific numbers for the visitor's cohort — trades a conversion cost for a retention gain that is several times larger.
Incorrect (onboarding language that hides the cold-start problem):
function WelcomeScreen() {
return (
<div>
<h2>Welcome — you're ready to go</h2>
<p>Start browsing now and book your first stay.</p>
<button>Continue</button>
</div>
)
}Correct (explicit cold-start warning with cohort-specific numbers):
async function WelcomeScreen({ role, targetSegment }: Props) {
const stats = await analytics.coldStartStats({ role, segment: targetSegment })
return (
<div>
<h2>A few things to expect</h2>
<p>
Your first {role === "sitter" ? "stay" : "booking"} is the hardest one —
new members typically complete it in{" "}
<strong>{stats.typicalDaysToFirst}</strong> days after applying to{" "}
<strong>{stats.typicalApplications}</strong> listings.
</p>
<p>
Once you have {stats.reviewsToUnlock} reviews, acceptance rates roughly{" "}
<strong>triple</strong>. Most members report the experience getting easier fast.
</p>
<button>I understand, continue</button>
</div>
)
}Attribute Conversion to the Signal That Changed the Profile
Standard last-click attribution answers "which page converted" but not "which intervention actually changed the visitor's mind". For pre-member personalisation, the more useful question is "which signal shift preceded the conversion?" — did the visitor become a member after seeing a specific local peer story, after a price-anchored comparison, after a cold-start honesty warning? Tracking the profile-feature diff across the session and correlating diffs with conversion identifies which interventions drive real lift, which is the only way to tell a working intervention apart from a lucky one.
Incorrect (last-click attribution identifies URL but not intervention):
def attribute_conversion(user_id: str) -> Attribution:
journey = session_log.get_journey(user_id)
return Attribution(
last_url=journey.events[-2].url,
referrer=journey.events[0].referrer,
)Correct (intervention-level attribution based on profile-feature diff):
def attribute_conversion(user_id: str) -> Attribution:
journey = session_log.get_journey(user_id)
profile_snapshots = journey.profile_snapshots
interventions_shown = journey.interventions_shown
profile_shifts = []
for i in range(1, len(profile_snapshots)):
diff = compare_profiles(profile_snapshots[i - 1], profile_snapshots[i])
if diff.magnitude > 0.1:
profile_shifts.append((profile_snapshots[i].timestamp, diff, interventions_shown[i]))
return Attribution(
primary_intervention=profile_shifts[-1][2] if profile_shifts else None,
intervention_chain=[shift[2] for shift in profile_shifts],
final_profile=profile_snapshots[-1],
)Reference: Kohavi, Tang, Xu — Trustworthy Online Controlled Experiments
Define Anonymous-to-Member Conversion as the Primary Outcome
Kohavi's research on trustworthy online experiments shows that organisations that optimise proxy metrics (clicks, sessions, sign-ups) consistently drift away from the outcome that actually matters, and the fix is to define a single primary outcome at the start of every experiment and refuse to ship on proxy wins alone. For pre-member personalisation, the only outcome that matters is whether the visitor becomes a paying member. Page views, clicks, time-on-site and even registrations are inputs — useful for diagnosis but not for ship decisions. Every pre-member experiment should declare "anonymous-to-member conversion" as the primary metric and treat everything else as a diagnostic.
Incorrect (proxy metric treated as ship criterion):
def evaluate_experiment(experiment: Experiment) -> Decision:
metrics = experiment.metrics()
if metrics["click_through_rate"].treatment > metrics["click_through_rate"].control:
return Decision.SHIP
return Decision.KILLCorrect (primary outcome is conversion, proxies are diagnostics):
def evaluate_experiment(experiment: Experiment) -> Decision:
metrics = experiment.metrics()
primary = metrics["anonymous_to_member_conversion"]
if primary.p_value >= 0.05:
return Decision.INCONCLUSIVE
if primary.relative_lift < 0.01:
return Decision.KILL
if any_guardrail_regressed(metrics, guardrails=["registration_rate", "session_success"]):
return Decision.INVESTIGATE
return Decision.SHIPReference: Kohavi, Tang, Xu — Trustworthy Online Controlled Experiments
Run Interleaving for Fast Pre-Member Experiments
Radlinski and Craswell's research on interleaved evaluation (WSDM 2013) showed that interleaving — alternating items from two ranking variants within the same session — delivers statistically significant results with 10-100x less traffic than full A/B testing. For pre-member surfaces, where traffic is usually the bottleneck (high-intent visitors are the minority), interleaving is the right primitive for ranking-quality experiments because it lets the team iterate at the pace of a week instead of a quarter. The trade-off is that interleaving answers "which variant is preferred within a session" not "which variant produces better downstream outcomes", so the team runs interleaving for ranking and full A/B for conversion.
Incorrect (full A/B test for every ranking change, slow iteration):
def evaluate_ranking_change(variant_a: Ranker, variant_b: Ranker) -> Report:
experiment = ab_test.create(
variants={"a": variant_a, "b": variant_b},
allocation={"a": 0.5, "b": 0.5},
primary_metric="click_through_rate",
)
return experiment.wait_for_significance(min_sample_size=50_000)Correct (interleaving for ranking iterations, full A/B for conversion gates):
def evaluate_ranking_change(variant_a: Ranker, variant_b: Ranker) -> Report:
interleaved = interleaving.create(
variants=[variant_a, variant_b],
method="team_draft",
primary_metric="clicks_by_variant",
)
quick_report = interleaved.wait_for_significance(min_sample_size=5_000)
if quick_report.winner is None:
return quick_report
full = ab_test.create(
variants={"control": variant_a, "treatment": quick_report.winner},
allocation={"control": 0.5, "treatment": 0.5},
primary_metric="anonymous_to_member_conversion",
)
return full.wait_for_significance(min_sample_size=40_000)Reference: Radlinski and Craswell — Optimized Interleaving for Online Retrieval Evaluation (WSDM 2013)
Segment Conversion Measurement by Channel and Visitor Profile
Aggregate conversion rates hide segment-level reality with alarming regularity — organic search visitors might convert at 8% while paid social visitors convert at 2%, and a blended 4% number tells the team nothing about where to invest. Simpson's paradox is routine in pre-member experiments: an intervention that lifts aggregate conversion 3% might lift organic 5% and drop paid social 1%, and a team optimising aggregate misses both facts. Slicing every primary metric by acquisition channel, inferred role, target destination and visitor cohort surfaces the real drivers and prevents ship decisions that are right on average and wrong in specifics.
Incorrect (aggregate conversion rate is the only number tracked):
def weekly_conversion() -> dict:
return {
"anonymous_to_member_rate": analytics.conversion_rate(
event_a="page_view",
event_b="membership_activated",
window_days=30,
),
}Correct (segmented by channel, role, target and cohort):
def weekly_conversion() -> dict:
segments = {
"overall": {},
"by_channel": ["organic_search", "paid_social", "paid_search", "referral", "direct"],
"by_role": ["owner", "sitter", "both", "unknown"],
"by_target": ["local", "european", "international", "unknown"],
"by_cohort": ["first_session", "returning_anonymous", "registered_not_paid"],
}
results: dict = {}
for axis, values in segments.items():
if not values:
results[axis] = analytics.conversion_rate(event_a="page_view", event_b="membership_activated")
continue
results[axis] = {
value: analytics.conversion_rate(
event_a="page_view",
event_b="membership_activated",
filter={axis.replace("by_", ""): value},
)
for value in values
}
return resultsReference: Kohavi, Tang, Xu — Trustworthy Online Controlled Experiments
Allow Answer Revision Without Restart
Visitors change their minds mid-onboarding — they realise the dates they entered are wrong, they want to try a different destination, they meant to pick owner instead of both. A form that does not let the visitor revise previous answers without losing progress forces them to choose between completing a flow they no longer believe in, starting over, or abandoning. Most pick abandon. Persist the in-progress state, show a visible breadcrumb of previous answers, and let the visitor click any previous step to revise without losing the rest.
Incorrect (linear form with no way back without losing progress):
function OnboardingFlow() {
const [step, setStep] = useState(0)
const [answers, setAnswers] = useState<Partial<Answers>>({})
return (
<div>
<Step currentStep={step} answers={answers} setAnswers={setAnswers} />
<Button onClick={() => setStep(step + 1)}>Continue</Button>
</div>
)
}Correct (persistent state, breadcrumb, revision-without-restart):
function OnboardingFlow() {
const [step, setStep] = useState(0)
const [answers, setAnswers] = usePersistedState<Partial<Answers>>("onboarding", {})
return (
<div>
<Breadcrumb
steps={ONBOARDING_STEPS}
currentStep={step}
completedSteps={Object.keys(answers)}
onStepClick={(targetStep) => setStep(targetStep)}
/>
<Step currentStep={step} answers={answers} setAnswers={setAnswers} />
<ButtonRow>
{step > 0 && <Button variant="secondary" onClick={() => setStep(step - 1)}>Back</Button>}
<Button onClick={() => setStep(step + 1)}>Continue</Button>
</ButtonRow>
</div>
)
}Ask the Highest-Information-Gain Question Earliest
Every onboarding question costs friction measured as drop-off, and every question produces some personalisation lift. The optimal question order is descending information gain — the question whose answer most changes the downstream experience goes first, so that visitors who drop out still leave the system with the most valuable answer. For an owner, the highest-gain question after role is typically city plus pet type; for a sitter, it is target destination plus rough date window. Placing low-gain demographic questions before high-gain targeting questions wastes the most-engaged moment of the flow.
Incorrect (low-gain demographic questions before high-gain targeting questions):
const OWNER_STEPS = [
{ field: "firstName", required: true },
{ field: "lastName", required: true },
{ field: "dateOfBirth", required: false },
{ field: "howDidYouHear", required: true },
{ field: "city", required: true },
{ field: "petType", required: true },
]Correct (high-information-gain questions first):
const OWNER_STEPS = [
{ field: "city", required: true, informationGainScore: 0.9 },
{ field: "petType", required: true, informationGainScore: 0.85 },
{ field: "travelDateWindow", required: true, informationGainScore: 0.75 },
{ field: "firstName", required: true, informationGainScore: 0.2 },
{ field: "email", required: true, informationGainScore: 0.3 },
{ field: "howDidYouHear", required: false, informationGainScore: 0.05 },
]
OWNER_STEPS.sort((a, b) => b.informationGainScore - a.informationGainScore)Reference: Luke Wroblewski — Web Form Design: Filling in the Blanks
Ask Role Before Any Other Onboarding Question
Role is the single largest branching factor in a two-sided marketplace — every subsequent question depends on whether the visitor is on the supply or demand side. Luke Wroblewski's research on progressive form design shows that high-branching questions belong first because the answer changes which questions follow. Asking a generic question (name, email) before role forces the system to show the same follow-up fields to everyone and hides the branching from the visitor, which is a missed opportunity to tailor the remainder of the flow dramatically.
Incorrect (generic fields first, role later):
function OnboardingStep1() {
return (
<Form>
<Field name="firstName" label="First name" />
<Field name="email" label="Email" />
<Field name="password" label="Password" />
<Button>Continue</Button>
</Form>
)
}Correct (role chosen first, pre-filled from inference, drives branching):
function OnboardingStep1({ inferredRole, inferredRoleConfidence }: Props) {
const [role, setRole] = useState<Role | null>(
inferredRoleConfidence >= 0.7 ? inferredRole : null
)
return (
<div>
<h2>What brings you here?</h2>
<RoleChooser value={role} onChange={setRole}>
<Choice value="owner">I have a pet and need care while I travel</Choice>
<Choice value="sitter">I want to travel and look after pets</Choice>
<Choice value="both">Both — I have pets and I want to travel</Choice>
</RoleChooser>
{role && <Button onClick={() => goToRoleSpecificStep2(role)}>Continue</Button>}
</div>
)
}Reference: Luke Wroblewski — Web Form Design: Filling in the Blanks
Make Optional Questions Genuinely Skippable
Nielsen Norman Group's research on forms consistently shows that optional questions marked with required-field styling (asterisks, red borders, "required" labels on optional fields) trigger abandonment at the same rate as genuinely required fields, because users cannot tell the difference quickly. If a question does not branch the downstream experience — a demographic, an interest tag, an optional preference — it should be obviously skippable, with an explicit "Skip" button and no styling that implies required. The alternative is a dark pattern that trades completion-rate for abandonment-rate and usually loses.
Incorrect (optional field styled identically to required, no skip button):
function OnboardingInterestStep() {
const [interest, setInterest] = useState("")
return (
<Form onSubmit={() => goNext(interest)}>
<Field
label="What are your interests? *"
required
value={interest}
onChange={setInterest}
/>
<Button type="submit">Continue</Button>
</Form>
)
}Correct (optional field explicit, skip button prominent):
function OnboardingInterestStep() {
const [interest, setInterest] = useState("")
return (
<Form onSubmit={() => goNext(interest)}>
<Field
label="What are your interests?"
optionalLabel="Optional — helps us recommend stays"
value={interest}
onChange={setInterest}
/>
<ButtonRow>
<Button variant="secondary" onClick={() => goNext(null)}>Skip</Button>
<Button type="submit">Continue</Button>
</ButtonRow>
</Form>
)
}Prefill Onboarding Answers from Inferred Signal
Signals captured earlier in the session — URL path, geo-IP, entry-point metadata, clicks — already answer some of the questions onboarding is about to ask. A visitor from the /find-a-sitter-london URL path is almost certainly an owner in London, and asking them to type London into a blank field is a small insult that adds drop-off. Pre-fill every field the system can credibly infer, show the inference source transparently, and let the visitor confirm or correct. This turns onboarding from a data-entry exercise into a confirmation exercise, which has dramatically lower friction.
Incorrect (blank fields, no use of prior inference):
function OnboardingCityStep() {
const [city, setCity] = useState("")
return (
<Field
label="Which city?"
value={city}
onChange={setCity}
placeholder="e.g. London"
/>
)
}Correct (inferred value pre-filled with transparent source and easy correction):
function OnboardingCityStep({ profile }: { profile: InferredProfile }) {
const inferred = profile.inferredCity
const [city, setCity] = useState(inferred?.value ?? "")
return (
<div>
<Field label="Which city?" value={city} onChange={setCity} />
{inferred && city === inferred.value && (
<Caption>
Inferred from your {inferred.source === "geoip" ? "location" : "search"}.
<Button variant="inline" onClick={() => setCity("")}>
Change
</Button>
</Caption>
)}
</div>
)
}Reference: Auth0 — Progressive Profiling
Anchor Membership Cost Against the Visitor's Local Kennel Alternative
Kahneman and Tversky's price-anchoring research shows that the evaluation of a price depends on the reference point it is compared against, and that reference point is chosen implicitly by whichever number the user sees first. An owner seeing "£129/year" in isolation compares it to other annual subscriptions and hesitates; the same owner seeing "£129/year — about 3 nights in a local kennel" instantly reframes the number as a saving. The critical detail is that the anchor must be the visitor's local kennel cost, because London kennels at £50/night make a different anchor than rural Welsh kennels at £18/night, and a global average fails for both.
Incorrect (abstract price with no anchor):
function PricingCard() {
return (
<div>
<h2>Membership</h2>
<p className="price">£129 / year</p>
<button>Join now</button>
</div>
)
}Correct (local-kennel anchor personalised to the visitor's region):
async function PricingCard({ visitorRegion }: { visitorRegion: string }) {
const kennelRate = await kennels.averageNightlyRate(visitorRegion)
const equivalentNights = Math.round(129 / kennelRate)
return (
<div>
<h2>Membership</h2>
<p className="price">£129 / year</p>
<p className="anchor">
About {equivalentNights} nights in a local kennel (£{kennelRate}/night average in {visitorRegion}).
</p>
<p className="usage">Members book 6-12 nights of sitting per year on average.</p>
<button>Join now</button>
</div>
)
}Reference: Kahneman and Tversky — Prospect Theory (Econometrica 1979)
Demystify Owner Effort Explicitly Before Payment
The Fogg Behavior Model (Fogg 2009) states that behaviour happens when motivation, ability, and a trigger converge — and ability is governed largely by perceived effort. A pet owner considering the platform is comparing it mentally to kennels, which have one decision (pick the kennel) and one handover (drop off the pet). The platform sounds harder, and the marketing typically obscures the effort with aspirational language. Showing the actual effort breakdown honestly — writing a listing, reviewing applications, interviewing, handing over the home — counterintuitively increases conversion because it replaces unknown effort (which the brain overweights) with known effort.
Incorrect (aspirational copy that hides the effort):
function HowItWorks() {
return (
<Steps>
<Step title="Post a listing" description="Tell us about your home and pet" />
<Step title="Meet sitters" description="Connect with trusted sitters" />
<Step title="Enjoy your trip" description="Travel with peace of mind" />
</Steps>
)
}Correct (explicit time budget against each step with honest framing):
function HowItWorks() {
return (
<Steps>
<Step
title="Write your listing"
timeEstimate="15-20 minutes"
description="Describe your home, your pet and the dates. Photos help a lot."
/>
<Step
title="Review applications"
timeEstimate="15-30 minutes over 2-5 days"
description="Typical listings get 3-8 sitter applications. You read profiles and pick favourites."
/>
<Step
title="Video interview"
timeEstimate="20-30 minutes"
description="A call with one or two shortlisted sitters to confirm fit."
/>
<Step
title="Hand over the home"
timeEstimate="45 minutes in person"
description="Show the sitter the pet routine, the keys, the house rules."
/>
</Steps>
)
}Display Honest Local Availability, Not Inflated Global Counts
Expectancy-violation research (Burgoon 1993) and the broader literature on consumer trust show that discovering a service is worse than advertised after payment burns trust more severely than the equivalent disappointment before payment. An owner who pays for membership expecting "hundreds of sitters in your area" and finds four is not just disappointed — they feel deceived, and the churn rate on expectation-violated cohorts runs 2-3× the baseline. Showing honest local availability pre-payment costs some conversions but dramatically improves first-year retention, which is what drives lifetime value on a subscription marketplace.
Incorrect (global count shown to every visitor regardless of their location):
def trust_bar(request: Request) -> TrustBar:
return TrustBar(
headline="Over 200,000 trusted sitters worldwide",
secondary=f"Join {platform.total_owners} members today",
)Correct (honest local availability against the visitor's region and dates):
def trust_bar(request: Request) -> TrustBar:
geo = request.profile.geoip_region
period = request.profile.inferred_travel_window or next_60_days()
local = sitters.active_in(
region=geo,
available_during=period,
accepting_new_owners=True,
)
if len(local) >= 20:
return TrustBar(headline=f"{len(local)} sitters available in {geo} for your dates")
if len(local) >= 5:
return TrustBar(headline=f"{len(local)} sitters active in {geo} — limited availability, book early")
return TrustBar(
headline=f"{len(local)} sitters active in {geo}",
alternatives=nearby_regions_with_more_supply(geo, period, limit=3),
honest_warning="Supply is thin for your dates. Consider flexible dates or nearby areas.",
)Reference: Burgoon — Interpersonal Expectations, Expectancy Violations, and Emotional Communication
Rank Sitters by Experience with the Visitor's Pet Type
Matching-market research (Roth, Who Gets What and Why) shows that two-sided matching quality depends on both sides' acceptance probabilities, and visitor owners are making an implicit acceptance calculation during preview browsing — "would this sitter accept my specific pet?" A sitter profile that generically says "experienced with pets" does not answer that question for an owner whose dog is an elderly diabetic Great Dane. Ranking preview listings by the sitter's demonstrated experience with the visitor's pet type (species, breed class, age band, health flags) closes the feasibility question concretely and removes the single biggest pre-payment objection.
Incorrect (ranking by global popularity ignores the visitor's pet type):
def preview_sitters(visitor: AnonVisitor, region: str) -> list[Sitter]:
return sitters.query(
region=region,
sort=[("completed_stays", "desc")],
limit=24,
)Correct (ranking by demonstrated experience with the visitor's pet type):
def preview_sitters(visitor: AnonVisitor, region: str) -> list[Sitter]:
pet_type = visitor.profile.get("pet_type") or visitor.profile.get("inferred_pet_type")
if not pet_type:
return sitters.query(region=region, sort=[("completed_stays", "desc")], limit=24)
candidates = sitters.query(region=region, limit=200)
return sorted(
candidates,
key=lambda s: (
-s.completed_stays_for_pet_type(pet_type),
-s.completed_stays_for_pet_size(pet_type.size_class),
-s.average_rating,
),
)[:24]Reference: Alvin Roth — Who Gets What and Why: The New Economics of Matchmaking and Market Design
Show Specific Local Owner Reviews, Not Global Averages
Cialdini's foundational research on social influence shows that specific peer examples convert far more than aggregate statistics, and the identifiable-victim effect (Small and Loewenstein 2003) confirms that a named person with a photo is psychologically weightier than any statistic. A pre-member owner considering paying for the platform is running a risk calculation on their pet's wellbeing, and "trusted by 100,000 members" does not answer "does this work for people like me". A localised, specific review from another owner in the visitor's own city, with a real photo and a real stay date, does.
Incorrect (aggregate trust signal, no localised evidence):
function TrustBlock({ stats }: { stats: PlatformStats }) {
return (
<div>
<h3>Trusted by {stats.totalOwners} owners worldwide</h3>
<p>Average rating: {stats.averageRating} across {stats.totalStays} stays</p>
<StarRating value={stats.averageRating} />
</div>
)
}Correct (specific, localised, named evidence):
async function TrustBlock({ visitorCity }: { visitorCity: string }) {
const recentStay = await reviews.pickRecentOwnerReview({
cityWithin: visitorCity,
maxRadiusKm: 10,
minRating: 4,
includeMildCriticism: true,
})
return (
<div>
<h3>Last week in {recentStay.city}</h3>
<OwnerAvatar name={recentStay.ownerFirstName} city={recentStay.city} />
<blockquote>{recentStay.quote}</blockquote>
<p>{recentStay.ownerFirstName} booked {recentStay.sitterFirstName} for
a {recentStay.nights}-night stay with their {recentStay.petType}.</p>
</div>
)
}Surface Safety Guarantees and Insurance Before Listings
Slovic's affect-heuristic research and Kahneman's work on probability weighting both show that humans dramatically overweight rare negative outcomes when evaluating risk, especially when the outcome involves someone or something they love. A pet owner considering leaving their dog with a stranger is not doing an expected-value calculation — they are imagining the worst case and assigning it disproportionate weight. Explicit, prominent insurance coverage, emergency support, and a clear dispute resolution path directly counter this cognitive bias and are among the highest-leverage conversion levers for pet owners specifically. These signals belong above the fold, before any listing is shown.
Incorrect (safety information buried in a footer or FAQ):
function HeroSection({ listings }: Props) {
return (
<>
<Hero title="Find a sitter" />
<ListingGrid listings={listings} />
<Footer links={["About", "Safety", "Terms"]} />
</>
)
}Correct (safety coverage surfaced prominently above the listings):
function HeroSection({ listings, visitorRole }: Props) {
return (
<>
<Hero title="Find a sitter for your pet" />
{visitorRole === "owner" && (
<SafetyStripe
items={[
{ icon: "shield", label: "£25,000 home and pet injury cover included" },
{ icon: "phone", label: "24/7 vet line and emergency support" },
{ icon: "check", label: "ID verification and police check on every sitter" },
{ icon: "arrow-back", label: "Full refund if a sitter cancels" },
]}
linkToPolicy="/safety"
/>
)}
<ListingGrid listings={listings} />
</>
)
}Reference: Slovic et al — The Affect Heuristic
Build Profile Features Incrementally on Each Interaction
An anonymous visitor reveals preferences with every scroll, click and dwell — but only if the system accumulates those signals into a live profile rather than treating every page as an isolated request. A visitor who clicks three listings in London, ignores a row in Paris, and dwells on a listing that accepts large dogs has told the system enough to rank the next page dramatically better than a cold-start global popularity list. The profile feature store should update on every event, expose the current feature vector to every downstream request, and mutate cheaply enough that every interaction refines the next recommendation.
Incorrect (each page served from global popularity, clicks ignored):
def homefeed(request: Request) -> list[Listing]:
return listings.top_by_global_popularity(limit=24)
def on_click(request: Request, listing_id: str) -> None:
analytics.track("click", listing_id=listing_id)Correct (profile feature store updated on every event, read by next request):
def homefeed(request: Request) -> list[Listing]:
features = profile_store.get(request.anon_session)
return listings.rank_by_features(
features=features,
fallback="global_popularity",
limit=24,
)
def on_click(request: Request, listing_id: str) -> None:
listing = listings.get(listing_id)
profile_store.update(
anon_session=request.anon_session,
updates={
"clicked_regions": {"append": listing.region},
"clicked_price_tiers": {"append": listing.price_tier},
"clicked_species_accepted": {"append": listing.species_accepted},
"click_count": {"increment": 1},
"last_active_at": {"set": datetime.utcnow()},
},
)
analytics.track("click", listing_id=listing_id)Decay Profile Features with Session Inactivity
A click from 40 minutes ago in a single session is a weaker preference signal than a click from 20 seconds ago — the visitor's attention and intent have both shifted. Without time decay on in-session features, a visitor who starts by browsing Barcelona and then switches to Porto has their profile dominated by the earlier Barcelona clicks and the ranker keeps showing Barcelona listings long after the visitor moved on. Exponential decay with a half-life of a few minutes on click-derived features matches how attention actually shifts within a session and keeps the ranker tracking the current intent.
Incorrect (equal weight on every click in the session):
def compute_region_preference(clicks: list[ClickEvent]) -> dict[str, float]:
counts: dict[str, int] = {}
for click in clicks:
counts[click.region] = counts.get(click.region, 0) + 1
total = sum(counts.values())
return {region: count / total for region, count in counts.items()}Correct (exponential decay with a 5-minute half-life):
def compute_region_preference(clicks: list[ClickEvent], now: datetime) -> dict[str, float]:
half_life_seconds = 300
weights: dict[str, float] = {}
for click in clicks:
age_seconds = (now - click.timestamp).total_seconds()
weight = 0.5 ** (age_seconds / half_life_seconds)
weights[click.region] = weights.get(click.region, 0.0) + weight
total = sum(weights.values())
return {region: weight / total for region, weight in weights.items()} if total > 0 else {}Reference: Ensemble Contextual Bandits for Personalized Recommendation (ACM RecSys 2014)
Persist Anonymous Profile Across Tabs and Reloads
A visitor who refreshes the page, opens a listing in a new tab, or returns an hour later should find their accumulated profile intact — losing it forces the system to cold-start every tab and every return visit, which is exactly the state pre-member personalisation is supposed to escape. The profile must be persisted in a store keyed by the anonymous session token (not by tab state or in-memory Redux), so that any subsequent request for the same session can hydrate the full profile and continue where the previous request left off. This is the essential plumbing that makes progressive profiling actually work.
Incorrect (profile held in browser tab state, lost on refresh):
const profile = {
clickedRegions: [] as string[],
clickedSpecies: [] as string[],
}
function onListingClick(listing: Listing): void {
profile.clickedRegions.push(listing.region)
profile.clickedSpecies.push(listing.speciesAccepted)
rerankHomefeed(profile)
}Correct (profile persisted in a session-keyed store on the server):
async function onListingClick(anonSession: string, listing: Listing): Promise<void> {
await profileStore.update(anonSession, {
clickedRegions: { append: listing.region },
clickedSpecies: { append: listing.speciesAccepted },
lastActiveAt: { set: new Date().toISOString() },
})
await rerankHomefeed(anonSession)
}
async function hydrateProfile(anonSession: string): Promise<Profile> {
return profileStore.get(anonSession) ?? emptyProfile()
}Reference: Treasure Data — Real-Time ID Stitching
Reset Profile Features on Explicit Role Changes
A visitor who starts on the sitter side of the site and then clicks "actually I need a sitter" has revealed the system was wrong about their role — every click they made as an inferred sitter is now misleading evidence for the owner experience they actually want. Continuing to accumulate the two sets of clicks into one profile produces a muddled ranker that satisfies neither side. The fix is to reset role-specific profile features on any explicit role change while preserving stable signals (language, region, device), so the switch feels like a clean start to the visitor and the ranker rebuilds the right side of the profile from the next interaction.
Incorrect (all clicks accumulated into one profile regardless of role switch):
def on_role_switch(anon_session: str, new_role: Role) -> None:
profile_store.update(
anon_session=anon_session,
updates={"inferred_role": {"set": new_role}},
)Correct (role-specific features cleared, stable features preserved):
ROLE_SPECIFIC_FEATURES = {
"clicked_listings",
"clicked_regions",
"clicked_species_accepted",
"dwell_durations",
"preview_viewed",
}
STABLE_FEATURES = {"language", "geoip_region", "device_type", "entry_point"}
def on_role_switch(anon_session: str, new_role: Role) -> None:
current = profile_store.get(anon_session)
preserved = {key: current.get(key) for key in STABLE_FEATURES}
profile_store.replace(
anon_session=anon_session,
profile={
**preserved,
"inferred_role": new_role,
"inferred_role_confidence": 1.0,
"inferred_role_source": "explicit_user_switch",
"role_switched_at": datetime.utcnow(),
},
)Reference: Auth0 — Progressive Profiling
Surface Profile Confidence Alongside Predictions
A profile feature computed from one click is not the same thing as a feature computed from thirty clicks, even if the point estimate looks identical. Downstream code that ranks listings, decides what to show, or times a paywall should know the confidence of each feature so it can respect uncertainty — a low-confidence "likely in London" prediction should trigger a region chooser, not hardcode London in the filter. Returning a confidence score alongside every feature prevents over-confident personalisation that looks broken to the visitor when it guesses wrong.
Incorrect (point estimates only, downstream code cannot distinguish certain from uncertain):
def get_profile(anon_session: str) -> dict:
profile = profile_store.get(anon_session)
return {
"top_region": most_common(profile.clicked_regions) or profile.geoip_region,
"top_species": most_common(profile.clicked_species),
"inferred_role": profile.inferred_role,
}Correct (each feature carries a confidence score):
def get_profile(anon_session: str) -> dict:
profile = profile_store.get(anon_session)
return {
"top_region": {
"value": most_common(profile.clicked_regions) or profile.geoip_region,
"confidence": region_confidence(profile),
"source": "clicks" if profile.clicked_regions else "geoip",
},
"top_species": {
"value": most_common(profile.clicked_species),
"confidence": min(1.0, len(profile.clicked_species) / 5),
"source": "clicks",
},
"inferred_role": {
"value": profile.inferred_role,
"confidence": profile.inferred_role_confidence,
"source": profile.role_signal_source,
},
}Localise Social Proof to the Visitor's Geography
Psychological-distance research (Construal Level Theory, Trope and Liberman 2010) shows that the persuasive weight of an example decreases with felt distance — temporal, spatial, social or hypothetical. A visitor in Bristol sees "12 owners in the BS postcode booked sitters this week" as a specific nearby fact; the same visitor sees "15,000 owners booked sitters this week globally" as an abstract aggregate. Localise the denominator of every social proof claim to the visitor's region, postcode or city, inferred from geo-IP or explicit onboarding, and the proof feels closer to the visitor's own decision.
Incorrect (global aggregate social proof shown to every visitor):
function LiveActivity() {
return (
<div>
<p className="tick">
<strong>15,427 bookings</strong> made on the platform this week
</p>
</div>
)
}Correct (local denominator driven by visitor geography):
async function LiveActivity({ visitorRegion }: { visitorRegion: string }) {
const local = await analytics.recentBookingActivity({
region: visitorRegion,
windowDays: 7,
})
if (local.bookings < 5) {
return (
<p className="tick">
<strong>{local.bookings} bookings</strong> this week in {visitorRegion} —
small and growing
</p>
)
}
return (
<p className="tick">
<strong>{local.bookings} bookings</strong> this week in {visitorRegion} from
members in {local.uniquePostcodes} postcodes
</p>
)
}Reference: Trope and Liberman — Construal-Level Theory of Psychological Distance (Psychological Review 2010)
Match Peer Stories to the Visitor's Inferred Cohort
The similarity principle within social-influence research (Cialdini) and matching-market research on peer effects both show that social proof is stronger when the peer visibly resembles the recipient on dimensions the recipient cares about. A first-time sitter in their 20s targeting city breaks is not persuaded by a retired couple's year-long countryside sit, and a pet owner with an elderly chronically-ill dog is not reassured by a story about a healthy puppy. Tag stories with cohort metadata (age band, role, pet type, target destination, lifecycle stage) and match the displayed story to the visitor's inferred cohort at runtime.
Incorrect (a single testimonial shown to every visitor regardless of cohort):
const TESTIMONIAL = {
text: "Joining changed how I travel. Highly recommend.",
name: "Emma",
city: "Manchester",
}
function Testimonial() {
return <Quote author={TESTIMONIAL.name} from={TESTIMONIAL.city}>{TESTIMONIAL.text}</Quote>
}Correct (story matched to inferred cohort from visitor profile):
async function Testimonial({ profile }: { profile: InferredProfile }) {
const cohort = {
role: profile.role,
ageBand: profile.inferredAgeBand,
petType: profile.petType,
targetRegion: profile.targetRegion,
}
const story = await stories.pickMatchingCohort(cohort, {
fallback: "role_only",
minMatchScore: 0.6,
})
return (
<Quote author={story.firstName} from={story.city} role={story.role}>
{story.quote}
</Quote>
)
}Reference: Robert Cialdini — Influence: The Psychology of Persuasion
Source Peer Stories from Real User History, Not Handpicked Marketing
Nielsen Norman Group's long-running research on user trust in testimonials shows that visitors have become highly skeptical of handpicked marketing content, and that skepticism hurts conversion materially. The antidote is to source stories programmatically from the real user history — pick recent members who successfully completed a first stay, sample across cohorts, include minor criticisms alongside praise, and refresh constantly so the displayed content is provably current. The pipeline becomes part of the infrastructure: a scheduled job reads the real history, tags it, and publishes it to the experience.
Incorrect (handpicked testimonials curated by marketing team once):
TESTIMONIALS = [
{"name": "Emma", "city": "Manchester", "quote": "Best decision ever!"},
{"name": "David", "city": "Bristol", "quote": "Changed how I travel."},
{"name": "Sophie", "city": "Edinburgh", "quote": "Absolutely brilliant."},
]
def pick_testimonial() -> dict:
return random.choice(TESTIMONIALS)Correct (data pipeline selects from real recent history, including mild criticism):
def build_weekly_peer_stories() -> list[PeerStory]:
candidates = members.query(
joined_within_months=6,
completed_first_stay=True,
left_a_review=True,
consented_to_featured_story=True,
)
stories = []
for member in candidates:
review = member.first_stay_review
story = PeerStory(
first_name=member.first_name,
city=member.city,
cohort_tags=derive_cohort_tags(member),
quote=review.selected_quote,
mild_criticism=review.mild_criticism_excerpt,
days_ago=(datetime.utcnow() - review.posted_at).days,
)
stories.append(story)
return sample_balanced_across_cohorts(stories, per_cohort=5)
# Run this job weekly; replace the served story set atomically.Reference: Nielsen Norman Group — Trust and the Importance of Honest Reviews
Surface Mixed-Positive Reviews, Not Only Five-Star
Nielsen Norman Group and the broader consumer-trust literature show that all-positive review sets trigger fake-review skepticism, and that credibility peaks when reviews include some mild criticism alongside praise (the "blemishing effect", Ein-Gar, Shiv, and Tormala 2012). Curating only five-star reviews is therefore counterproductive — it converts less well than showing a realistic mix. When selecting reviews to surface in preview or at the paywall, include at least one honest mild-criticism review alongside the praise, labelled as a real review. Visitors interpret the mild criticism as proof the praise is also real.
Incorrect (curated five-star reviews only):
def display_reviews(sitter: Sitter) -> list[Review]:
return sitter.reviews.filter(rating=5).order_by_recency().limit(3)Correct (mix of high rating and honest mild criticism):
def display_reviews(sitter: Sitter) -> list[Review]:
all_reviews = sitter.reviews.all()
top_rated = all_reviews.filter(rating=5).order_by_recency().limit(2)
mixed = (
all_reviews
.filter(rating__gte=3, rating__lt=5)
.filter(has_specific_criticism=True)
.order_by_recency()
.limit(1)
)
if not mixed:
return top_rated.limit(3)
return list(top_rated) + list(mixed)Use Specific Peer Stories at Decision Points, Not Aggregate Stats
Cialdini's Influence documents exhaustively that specific social proof — "Jane booked Sarah last Tuesday" — converts dramatically better than aggregate statistics — "4.9 stars across 100,000 stays". The mechanism is the representativeness heuristic: the visitor asks "does this work for people like me?" and a statistic answers "on average" while a specific person answers "yes, concretely". At decision moments, especially near the paywall or at onboarding completion, replace aggregate trust signals with a single named-person story that matches the visitor's inferred cohort.
Incorrect (aggregate stats at the decision moment):
function ConfirmationStep() {
return (
<div>
<h2>One step to go</h2>
<p>Join 200,000 members who rate us 4.9 stars</p>
<button>Complete signup</button>
</div>
)
}Correct (a specific recent named-person story at the decision moment):
async function ConfirmationStep({ visitorCohort }: { visitorCohort: Cohort }) {
const recent = await stories.pickRecentSuccess({
matchingCohort: visitorCohort,
maxAgeDays: 14,
})
return (
<div>
<h2>One step to go</h2>
<div>
<PeerAvatar name={recent.firstName} city={recent.city} />
<p>
{recent.firstName} in {recent.city} completed her first stay with{" "}
{recent.sitterFirstName} {recent.daysAgo} days ago.
</p>
</div>
<button>Complete signup</button>
</div>
)
}Reference: Robert Cialdini — Influence: The Psychology of Persuasion
Capture Entry-Point Metadata on Every Page Load
A visitor arriving from a paid Google ad for "dog sitter london" has a radically different intent profile from a visitor arriving from a friend's referral link or from an organic search for "what is a house sit". Each entry point carries priors the system can use immediately — intent specificity, urgency, commercial vs informational, role probability — but only if the full entry-point metadata (UTM parameters, referrer, campaign ID, landing path) is captured into the session on the very first page load and persisted so every subsequent event can reference it.
Incorrect (entry-point metadata lost after first render):
export async function onFirstPageLoad(req: Request): Promise<void> {
analytics.track("page_view", {
url: req.url.pathname,
user_agent: req.headers.get("user-agent"),
})
}Correct (entry-point captured into session, persisted for the life of the journey):
export async function onFirstPageLoad(req: Request): Promise<void> {
const entryPoint = {
landing_path: req.url.pathname,
referrer: req.headers.get("referer"),
utm_source: req.query.utm_source,
utm_medium: req.query.utm_medium,
utm_campaign: req.query.utm_campaign,
utm_term: req.query.utm_term,
gclid: req.query.gclid,
fbclid: req.query.fbclid,
first_seen_at: new Date().toISOString(),
}
await session.setOnce("entry_point", entryPoint)
analytics.track("page_view", {
url: req.url.pathname,
entry_point: entryPoint,
})
}Reference: Snowplow — Users and Identity Stitching
Classify Inbound Intent from the Acquisition Channel
A visitor coming from paid search on "dog sitter next week london" arrives with transactional intent — they want a specific outcome soon and the session should show realistic inventory fast. A visitor coming from an editorial blog post about "should you trust a stranger with your dog" arrives with informational intent — they are investigating whether the concept is safe and the session should show evidence of safety, not a booking funnel. The acquisition channel is enough to seed this classification before any interaction, and the classification should route the visitor to an intent-appropriate first page rather than a one-size-fits-all landing.
Incorrect (one landing template for every inbound channel):
def landing_page(request: Request) -> LandingPage:
return LandingPage(
template="default",
hero="Find your perfect sitter",
sections=["featured", "how_it_works", "testimonials"],
)Correct (channel-driven intent classification routes to the right template):
def landing_page(request: Request) -> LandingPage:
intent = classify_intent_from_channel(
utm_source=request.query.utm_source,
utm_medium=request.query.utm_medium,
utm_campaign=request.query.utm_campaign,
referrer_domain=domain_of(request.headers.get("referer")),
landing_path=request.url.pathname,
)
if intent == InboundIntent.TRANSACTIONAL:
return LandingPage(template="fast_book", hero="Sitters available for your dates", sections=["local_results", "cta"])
if intent == InboundIntent.SAFETY_INVESTIGATIVE:
return LandingPage(template="safety_first", hero="Trusted by 10,000+ owners", sections=["verification_flow", "reviews", "cta"])
if intent == InboundIntent.CURIOSITY:
return LandingPage(template="learn_more", hero="How house sitting works", sections=["how_it_works", "stories", "cta"])
return LandingPage(template="default", hero="Find your perfect sitter", sections=["featured", "how_it_works", "cta"])Reference: Optimizely — Contextual Bandits in Personalization
Related skills
FAQ
What does marketplace-pre-member-personalisation do?
marketplace-pre-member-personalisation: A skill for development. This provides functionality for development workflows.
When should I use marketplace-pre-member-personalisation?
When you need to use marketplace-pre-member-personalisation for development tasks, or when marketplace-pre-member-personalisation: a skill for development. this provides functionality for development workflows.
What are the main capabilities?
marketplace-pre-member-personalisation.