In digital wallets, the cold-start problem isn't just a UX inconvenience—it's a regulatory and business risk. A new user who receives irrelevant or non-compliant financial product recommendations during onboarding may never activate their account. Traditional collaborative filtering fails entirely when behavioral history is sparse or nonexistent. Yet most enterprise RAG implementations treat all users identically, retrieving from the same vector store with the same strategy regardless of data maturity. This article demonstrates how to build an Adaptive Recommendation Engine that dynamically switches retrieval strategies based on real-time user maturity assessment, using LangGraph state machines, ChromaDB multi-collection architecture, and specialized cold-start agents.

The Real-Time Use Case: New User Activation Flow

Scenario: A digital wallet acquires 50K new users monthly. During the first 72 hours post-KYC, users ask questions like "What should I do with my first deposit?" or "Is this app safe for savings?" Behavioral signals are near-zero: no transaction history, no product clicks, no risk assessment completion.

Why Standard RAG Fails:

The Solution: A User Maturity Classifier Agent that routes queries through fundamentally different retrieval pipelines—demographic inference, knowledge-graph-guided exploration, or full semantic RAG—based on real-time signal availability.

414

Part 1: The Cold-Start Taxonomy & Strategy Matrix

Cold-start isn't binary. We model four maturity states, each with a distinct retrieval strategy:

Maturity StateSignal AvailabilityRetrieval StrategyPrimary Data Source
True ColdZero behavior, incomplete KYCDemographic + Regulatory DefaultPre-approved onboarding content collection
Warm-ColdKYC complete, <3 transactionsInferred Profile + ExploratoryDemographic embeddings + product knowledge graph
Warming3-20 transactions, partial preferencesHybrid Semantic + Behavioral BoostUser profile collection + product catalog
MatureRich history, explicit preferencesFull Personalized RAGUser preference vectors + full product catalog

Key Insight: Cold-start isn't solved by better embeddings. It's solved by knowing when NOT to use embeddings and falling back to structured, regulator-safe defaults.

Part 2: Multi-Collection ChromaDB Architecture

import chromadb
from langchain_chroma import Chroma
from langchain_openai import OpenAIEmbeddings

client = chromadb.PersistentClient(path="./chroma_wallet")
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")

# Collection 1: Full product catalog (mature users)
product_store = Chroma(client=client, collection_name="financial_products", 
                       embedding_function=embeddings)

# Collection 2: User profiles (warming/mature users)
user_profile_store = Chroma(client=client, collection_name="user_profiles", 
                            embedding_function=embeddings)

# Collection 3: Onboarding & regulatory defaults (cold users)
# NEVER contains personalized recommendations—only pre-approved educational content
onboarding_store = Chroma(client=client, collection_name="onboarding_safe_content", 
                          embedding_function=embeddings)

# Collection 4: Demographic cohort prototypes (warm-cold users)
# Pre-computed embeddings for "young_professional_PH", "retiree_SG", etc.
cohort_store = Chroma(client=client, collection_name="demographic_cohorts", 
                      embedding_function=embeddings)

Part 3: Adaptive State Schema

The state carries both the user's current maturity assessment AND the strategy-specific retrieval results.

from typing import Annotated, List, Dict, Optional, Literal
from typing_extensions import TypedDict
from langgraph.graph.message import add_messages
from langchain_core.documents import Document

MaturityLevel = Literal["true_cold", "warm_cold", "warming", "mature"]

class WalletColdStartState(TypedDict):
    messages: Annotated[List, add_messages]
    user_id: str
    
    # === MATURITY ASSESSMENT ===
    maturity_level: MaturityLevel
    signal_inventory: Dict[str, bool]  # {"has_kyc": True, "has_transactions": False, ...}
    inferred_demographic: Optional[str]  # e.g., "young_professional_PH"
    
    # === STRATEGY-SPECIFIC RETRIEVAL ===
    retrieved_docs: List[Document]
    retrieval_strategy_used: str
    
    # === OUTPUT ===
    ranked_recommendations: List[Dict]
    confidence_score: float          # Lower for cold-start; surfaced to UI
    evaluation_reasoning: str
    
    # Orchestration
    agent_trace: List[str]

Part 4: Multi-Agent Implementation

Agent A: User Maturity Classifier

This agent runs FIRST and determines the entire downstream pipeline. It examines structured metadata, NOT embeddings.

from langchain_openai import ChatOpenAI
from datetime import datetime, timedelta

llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)

def maturity_classifier_node(state: WalletColdStartState):
    """Assess user maturity from structured signals. NO LLM needed for basic classification."""
    # In production, fetch from your user service / DB
    user_meta = get_user_metadata(state["user_id"])  
    
    signals = {
        "has_kyc": user_meta.get("kyc_status") == "approved",
        "has_risk_assessment": user_meta.get("risk_assessment_completed", False),
        "transaction_count": user_meta.get("transaction_count_30d", 0),
        "days_since_signup": (datetime.utcnow() - user_meta["signup_date"]).days,
        "has_explicit_preferences": len(user_meta.get("stated_goals", [])) > 0,
        "has_behavioral_history": user_meta.get("click_history_count", 0) > 5
    }
    
    # Deterministic classification—no hallucination risk
    if not signals["has_kyc"]:
        level = "true_cold"
    elif signals["has_kyc"] and signals["transaction_count"] < 3:
        level = "warm_cold"
    elif signals["transaction_count"] < 20 and not signals["has_explicit_preferences"]:
        level = "warming"
    else:
        level = "mature"
    
    # For warm-cold: infer demographic from KYC data (age, location, occupation)
    inferred_demo = None
    if level == "warm_cold":
        inferred_demo = infer_demographic_cohort(user_meta)  # Rule-based or lightweight model
    
    return {
        "maturity_level": level,
        "signal_inventory": signals,
        "inferred_demographic": inferred_demo,
        "agent_trace": [f"classifier: maturity={level}, signals={signals}"]
    }

Agent B: Strategy-Specific Retrieval Router

A single node that dispatches to the correct retrieval strategy based on maturity level.

def adaptive_retriever_node(state: WalletColdStartState):
    query = state["messages"][-1].content
    level = state["maturity_level"]
    
    if level == "true_cold":
        # STRATEGY: Safe onboarding content only. No product recommendations.
        docs = onboarding_store.similarity_search(query, k=3)
        strategy = "onboarding_safe_defaults"
        
    elif level == "warm_cold":
        # STRATEGY: Cohort-based retrieval + exploratory product suggestions
        # Find closest demographic cohort prototype
        cohort_docs = cohort_store.similarity_search(
            state["inferred_demographic"] or "general_new_user", k=1
        )
        # Then retrieve products appropriate for inferred cohort's risk profile
        cohort_risk = cohort_docs[0].metadata.get("typical_risk_tolerance", "conservative")
        docs = product_store.similarity_search(
            query, k=8,
            filter={"risk_rating": {"$in": ["low", "medium"]}, 
                    "eligible_kyc_levels": {"$in": ["basic", "full"]}}
        )
        # Prepend cohort context so synthesizer understands the inference
        docs = cohort_docs + docs
        strategy = "cohort_inferred_exploratory"
        
    elif level == "warming":
        # STRATEGY: Hybrid—semantic search boosted by emerging behavioral signals
        docs = product_store.similarity_search(query, k=10)
        # TODO: Re-rank by early behavioral affinity (click dwell time, saves)
        strategy = "hybrid_semantic_behavioral"
        
    else:  # mature
        # STRATEGY: Full personalized RAG
        user_docs = user_profile_store.get(ids=[state["user_id"]], include=["documents"])
        docs = product_store.similarity_search(query, k=15)
        strategy = "full_personalized_rag"
    
    return {
        "retrieved_docs": docs,
        "retrieval_strategy_used": strategy,
        "agent_trace": state["agent_trace"] + [f"retriever: strategy={strategy}, docs={len(docs)}"]
    }

Agent C: Confidence-Aware Synthesizer

Cold-start responses MUST communicate uncertainty. This agent adjusts tone and disclosure based on maturity.

from langchain_core.prompts import ChatPromptTemplate

synth_prompt = ChatPromptTemplate.from_messages([
    ("system", """You are a Digital Wallet Financial Advisor.
    
    USER MATURITY: {maturity_level}
    RETRIEVAL STRATEGY: {strategy}
    SIGNAL GAPS: {missing_signals}
    
    RESPONSE RULES BY MATURITY:
    - true_cold: ONLY provide educational/onboarding content. NEVER recommend specific products.
      End with: "Complete your profile to get personalized suggestions."
    - warm_cold: Frame recommendations as "exploratory options based on similar users."
      Always disclose: "These are general suggestions. Your preferences will refine results over time."
    - warming: Normal recommendations but note areas where more data would improve accuracy.
    - mature: Full confident recommendations with standard disclaimers.
    
    Retrieved Content: {docs}"""),
    ("human", "{query}")
])

def confidence_synthesizer_node(state: WalletColdStartState):
    missing = [k for k, v in state["signal_inventory"].items() if not v]
    
    chain = synth_prompt | llm
    response = chain.invoke({
        "maturity_level": state["maturity_level"],
        "strategy": state["retrieval_strategy_used"],
        "missing_signals": ", ".join(missing) if missing else "None",
        "docs": "\n---\n".join(d.page_content for d in state["retrieved_docs"]),
        "query": state["messages"][-1].content
    })
    
    # Confidence score reflects maturity + retrieval quality
    base_confidence = {"true_cold": 0.3, "warm_cold": 0.5, "warming": 0.7, "mature": 0.9}
    confidence = base_confidence[state["maturity_level"]]
    
    return {
        "ranked_recommendations": [{"content": response.content}],
        "confidence_score": confidence,
        "evaluation_reasoning": f"Maturity={state['maturity_level']}, Strategy={state['retrieval_strategy_used']}",
        "messages": [("assistant", response.content)]
    }

Step 5: Compile the Adaptive Graph

from langgraph.graph import StateGraph, START, END
from langgraph.checkpoint.postgres import PostgresSaver

workflow = StateGraph(WalletColdStartState)

workflow.add_node("classify_maturity", maturity_classifier_node)
workflow.add_node("adaptive_retrieve", adaptive_retriever_node)
workflow.add_node("compliance_gate", compliance_validator_node)  # Same as previous article
workflow.add_node("synthesize", confidence_synthesizer_node)

workflow.add_edge(START, "classify_maturity")
workflow.add_edge("classify_maturity", "adaptive_retrieve")
workflow.add_edge("adaptive_retrieve", "compliance_gate")

# Compliance gate still applies—but fallback differs by maturity
def post_compliance_route(state: WalletColdStartState):
    if state.get("compliance_passed", True):
        return "synthesize"
    # Cold users get onboarding redirect; mature users get safe fallback
    if state["maturity_level"] in ("true_cold", "warm_cold"):
        return "synthesize"  # Synthesizer already handles safe framing
    return "safe_fallback"

workflow.add_conditional_edges("compliance_gate", post_compliance_route)
workflow.add_edge("synthesize", END)

app = workflow.compile(checkpointer=PostgresSaver.from_conn_string("postgresql://..."))

Part 5: Measuring Cold-Start Quality Beyond CTR

Cold-start requires different metrics per maturity stage:

Maturity StagePrimary MetricTargetWhy Not CTR
True ColdOnboarding Completion Rate>80%Clicks on products before KYC = regulatory risk
Warm-ColdProfile Completion Rate + First Transaction Rate>60% / >40%Exploration clicks without conversion = failed inference
WarmingPreference Calibration ScoreCorrelation(ranked_position, engagement) > 0.5Need to validate behavioral signals are improving relevance
MatureConversion Rate × LTVBusiness targetStandard metric applies

Implementation: Tag every recommendation event with maturity_level and retrieval_strategy_used in your analytics pipeline. Run separate funnel analyses per cohort.

Production Hardening for Cold-Start

  1. Demographic Inference Guardrails: Never infer sensitive attributes (race, religion). Use only regulator-permitted signals (age bracket, jurisdiction, declared occupation). Log inference basis for audit.

  2. Onboarding Content Versioning: The onboarding_safe_content collection must be versioned and approved by compliance. Include approved_date and expiry_date metadata. Auto-expire stale content.

  3. Maturity Transition Triggers: When a user crosses from warm_coldwarming, trigger an async job to generate their first user profile embedding from accumulated signals. Don't wait for the next query.

  4. Confidence Score Surfacing: Expose confidence_score to the frontend. Render low-confidence responses with visual cues ("Based on limited info...") to manage expectations and build trust.

  5. Cold-Start A/B Testing: Test cohort inference strategies against pure onboarding defaults. Measure activation rate, not engagement. Some cohorts may perform worse than generic content—kill them fast.

Conclusion

Cold-start in fintech isn't a retrieval problem—it's a state awareness problem. By modeling user maturity as a first-class state variable, maintaining separate retrieval collections per maturity stage, and routing through adaptive agents that respect signal boundaries, you transform cold-start from a failure mode into a guided onboarding experience. The key architectural insight: your graph should know what it doesn't know. When signals are absent, the system shouldn't hallucinate personalization—it should gracefully degrade to safe, compliant, and explicitly transparent guidance. That transparency is what converts cold users into mature, trusting customers.