Skip to content

lenzenc/discovery

Repository files navigation

Discovery

Pre-meeting intelligence for financial advisors.

Discovery gives advisors at independent RIAs a research-backed briefing packet before every discovery meeting: a prospect snapshot with citations, likely financial profiles, and framework-tagged discovery questions — assembled from public-web research and optional prospect intake — so advisors walk in prepared instead of winging it.


Overview

Post-meeting tools (Jump, Zocks, Zeplyn) own transcription and note capture. Nothing exists for the pre-meeting problem: who is this person, what do they care about, and what should I ask? Morgan Stanley's Next Best Action does this at wirehouse scale; no equivalent exists for independent RIAs.

Discovery fills that gap for mid-market RIAs (20–100 advisors). The advisor adds a prospect, clicks "Generate Brief," and within ~60 seconds has a packet built from Exa web research and Claude AI synthesis. Optionally, the advisor sends the prospect a magic-link questionnaire; when the prospect submits (with documents if they choose), the brief regenerates incorporating their verified answers.


Key features

  • AI-researched prospect snapshot — Exa web research distilled into a narrative snapshot. Every factual claim carries a source citation; if there's no citation, the claim isn't included.
  • Financial profile archetypes — Matches the prospect to 1–2 of ~20 archetypes (tech exec with RSUs, business owner pre-exit, federal employee with pension, etc.) with typical financial concerns. Clearly flagged as "typical, not verified."
  • Framework-tagged discovery questions — Separate question sets generated by three parallel LLM calls: Kitces CLEAR questions, Kinder EVOKE questions, archetype-specific probes. Each question includes a rationale and suggested follow-ups.
  • Anti-goal prompts — Psychologically distinct from questions; 5–8 prompts that help advisors surface what the prospect wants to avoid, with guidance on raising them without triggering defensiveness.
  • NEA-aware framing tips — Reframing language, landmines to avoid, and advisor communication patterns tailored to the prospect's likely emotional context.
  • Magic-link prospect intake — No client portal, no login. Advisor sends a link; prospect fills an adaptive questionnaire and optionally uploads documents (1040, brokerage statements). Submit triggers brief regeneration incorporating the verified data.
  • Document extraction — Tax returns and brokerage statements are classified (Haiku), extracted (Sonnet with native PDF input), and merged into the brief. Sensitive fields (account numbers, SSNs) are redacted before storage.
  • PDF export — One-click download of the fully formatted brief.
  • Firm admin — Invite-only user management, role assignment (admin/advisor/assistant), per-firm branding (primary color, logo), and daily AI cost caps.

See it in action

To regenerate these screenshots: run make reset && make dev, then in a second terminal cd web && npm run docs:screenshots. Requires ANTHROPIC_API_KEY and EXA_API_KEY in .env.

1. Add a prospect — 30 seconds, no CRM required

Drop in a name and a few hints. Discovery handles the research.

Advisor creates a new prospect

2. Brief ready before the meeting

Click "Generate Brief." Within about a minute, the advisor has a researched packet — who the prospect is, what they probably care about financially, and exactly what to ask.

Prospect snapshot — A researched overview with citations, so the advisor walks in knowing who they're meeting:

Brief — prospect snapshot

Discovery questions — Framework-tagged questions (Kitces CLEAR, Kinder EVOKE, archetype probes) with rationales — exactly what to ask:

Brief — discovery questions

Anti-goals — Prompts that surface what the prospect wants to avoid, with guidance on raising them without triggering defensiveness:

Brief — anti-goals

Framing tips — Reframing language and landmines to avoid, tuned to the prospect's likely emotional context:

Brief — framing tips

3. Optional: send the prospect a short questionnaire

The advisor sends a secure link — no login, no app. The prospect fills a short adaptive questionnaire from their phone or laptop, and the brief automatically regenerates with their verified answers.

Single-choice question (employment, income, risk tolerance, and more):

Intake — single choice question

Open-ended question (financial goals, career background):

Intake — open text question

Scale question (importance of legacy, charitable giving):

Intake — scale slider question

Multi-select question (top concerns, upcoming life transitions):

Intake — multi-select question

Optional document upload (tax return, brokerage statement):

Intake — document upload prompt

All done — responses sent to the advisor:

Intake — thank you screen


What's in v1 / out of v1

Included in v1: Prospect CRUD, full AI pipeline (8 steps), magic-link intake, document extraction (tax returns + brokerage statements), PDF export, firm admin, cost cap enforcement, Sentry observability.

Deliberately deferred: Plaid/account aggregation, CRM integrations (Redtail, Wealthbox, Salesforce FSC), voice transcript ingestion, client logins/portal, mobile apps, SOC 2, pgvector similarity search, LLM-in-the-loop intake, multi-language, scheduling integration, fine-tuning, brief templates with firm-customizable sections.


How it works

sequenceDiagram
    participant A as Advisor (browser)
    participant API as FastAPI
    participant Q as Redis / Arq
    participant P as AI Pipeline
    participant DB as Postgres

    A->>API: POST /prospects (create prospect)
    A->>API: POST /briefs (generate brief)
    API-->>A: 202 { brief_id, enrichment_id }
    API->>Q: enqueue enrich_prospect job

    loop Pipeline steps (8 sequential)
        Q->>P: run step
        P->>DB: persist enrichment_step output
    end
    P->>DB: write Brief (status=ready)

    loop Polling
        A->>API: GET /briefs/{id}
        API-->>A: { status: "generating" | "ready" }
    end
    A->>API: GET /briefs/{id}/pdf
    API-->>A: PDF download

    Note over A,DB: Optional intake flow
    A->>API: POST /intakes (send questionnaire)
    API->>Q: enqueue send_magic_link
    Q-->>A: email with magic link

    Note over A,DB: Prospect completes intake
    A->>API: POST /public/intake/consume (token)
    A->>API: answer questions + upload documents
    A->>API: POST /public/intake/submit
    API->>Q: enqueue enrich_prospect (with intake data)
    P->>DB: write updated Brief
Loading

Architecture

flowchart TB
    subgraph Browser
        adv[Advisor App\nnext.js :3000]
        pub[Public Intake\nnext.js /intake/token]
    end

    subgraph API["FastAPI :8000"]
        v1["/api/v1\n(JWT-authed)"]
        public["/api/public\n(token-authed)"]
    end

    subgraph Worker["Arq Worker"]
        jobs["enrich_prospect\nextract_document\nsend_magic_link\nexpire_magic_links (cron)"]
    end

    subgraph Data
        pg[(Postgres 16\n+ pgvector\nRLS policies)]
        redis[(Redis 7\nArq broker\nrate limits)]
        minio[(MinIO / S3\ndocuments)]
        smtp[SMTP\nMailhog / Resend]
    end

    subgraph AI
        claude[Anthropic Claude\nSonnet 4.6 / Haiku]
        exa[Exa Search]
        tavily[Tavily fallback]
    end

    adv --> v1
    pub --> public
    v1 --> pg
    v1 --> redis
    public --> pg
    Worker --> pg
    Worker --> redis
    Worker --> minio
    Worker --> smtp
    Worker --> claude
    Worker --> exa
    Worker --> tavily
Loading

AI pipeline

flowchart LR
    inp([PipelineInput])

    inp --> IR[identity_resolution]
    IR --> WR[web_research]
    WR --> PS[profile_synthesis]
    PS -->|"💰 cost cap check"| FP[financial_profile]
    PS --> QG[questions_generator\n3 parallel LLM calls]
    PS --> AG[anti_goals]
    PS --> NEA[nea_framing]
    FP --> BC
    QG --> BC
    AG --> BC
    NEA -->|"💰 cost cap check"| BC[brief_composer]
    BC --> out([Brief persisted\nstatus=ready])
Loading

Each step persists its Pydantic-typed output to enrichment_steps before the next begins. Cost-cap guards abort the pipeline if per-enrichment or per-firm daily spend is exceeded.


Tech stack

Layer Choice Why
Backend Python 3.14 · FastAPI · Pydantic v2 Async-first; structured output maps directly to Pydantic models
Frontend Next.js 15 App Router · MUI v6 · TanStack Query v5 SSR + client islands; MUI covers advisor-app fidelity needs
Database Postgres 16 + pgvector RLS for multi-tenancy; pgvector ready for future similarity search
Migrations Alembic async Integrates with SQLAlchemy 2 async engine
Jobs Arq Asyncio-native, Redis already in stack; simpler than Celery
AI Anthropic Claude (Sonnet 4.6 / Haiku) Structured output via tool-use; prompt caching cuts cost significantly
Web search Exa (primary) · Tavily (fallback) Exa neural search quality; Tavily as protocol-compatible fallback
Storage MinIO (dev) · S3 (prod) Presigned PUT URLs; server never streams files
Email Mailhog (dev) · Resend / SES (prod) SMTP interface; swappable without code changes
Auth fastapi-users + invite-only JWT Home-rolled avoids SSO vendor lock-in for v1

Repo layout

discovery/
├── app/                          # FastAPI application
│   ├── api/v1/                   # Thin routers: auth, firms, users, prospects, briefs, intakes, documents
│   ├── domain/                   # Business logic (no FastAPI, no DB sessions — unit-testable)
│   │   ├── enrichment/           # AI pipeline: orchestrator.py + steps/
│   │   ├── intake/               # Adaptive question graph + service
│   │   └── documents/            # Document extractors (tax_return, brokerage_statement)
│   ├── integrations/
│   │   ├── anthropic/            # Structured-output client + prompt library (prompts/v1/)
│   │   └── web_search/           # Exa + Tavily behind a Protocol interface
│   ├── db/models/                # SQLAlchemy ORM: firm, user, prospect, enrichment, brief, intake, document
│   ├── jobs/                     # Arq WorkerSettings + tasks/
│   └── security/                 # RLS session setter, magic-link tokens, Fernet encryption, rate limiting
├── migrations/                   # Alembic async env + revision files
├── tests/                        # unit/, integration/ (incl. multi-tenant isolation gate)
├── scripts/                      # seed_dev.py, reset_db.py, init_db.sql
├── docker-compose.yml            # Postgres, Redis, MinIO, Mailhog (app runs on host)
├── Makefile
└── web/                          # Next.js frontend
    ├── app/(advisor)/            # Authenticated advisor shell: dashboard, prospects/[id], settings
    ├── app/intake/[token]/       # Public magic-link intake surface (no login)
    ├── components/brief/         # BriefView, SnapshotCard, QuestionCard, AntiGoalCard, …
    ├── components/intake/        # IntakeStatusCard, SendIntakeDialog
    └── lib/                      # api.ts (typed fetch client), queries.ts (TanStack hooks)

Quickstart

Prerequisites

  • Python 3.14 (managed by uv)
  • uvcurl -LsSf https://astral.sh/uv/install.sh | sh
  • Node 20+
  • Docker (for Postgres, Redis, MinIO, Mailhog)

1. Clone and configure

git clone <repo-url> && cd discovery
cp .env.example .env

Edit .env and fill in the required values:

# Generate SECRET_KEY
python -c "import secrets; print(secrets.token_hex(32))"

# Generate ENCRYPTION_KEY
python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"

Also add your ANTHROPIC_API_KEY and at least one of EXA_API_KEY / TAVILY_API_KEY.

2. Install dependencies

make deps          # uv sync --all-extras + cd web && npm install

3. Start dependencies and migrate

make up            # starts Postgres, Redis, MinIO, Mailhog
make migrate       # applies Alembic migrations

4. (Optional) Seed dev data

uv run python scripts/seed_dev.py   # creates a dev firm + admin user

5. Run everything

make dev           # FastAPI :8000 + Arq worker + Next.js :3000 (all in one)

Or start processes individually:

make dev-backend   # FastAPI only
make dev-worker    # Arq worker only
make dev-frontend  # Next.js only

Service URLs

Service URL
Advisor app http://localhost:3000
API + Swagger docs http://localhost:8000/docs
Mailhog (email preview) http://localhost:8025
MinIO console http://localhost:9001
Adminer (DB browser) docker compose --profile tools up -dhttp://localhost:8080

Common tasks

Command What it does
make test Full test suite with coverage
make test-fast Fast run, no coverage (-x -q)
make lint ruff check + format check
make lint-fix ruff auto-fix + reformat
make typecheck mypy on app/domain, app/db, app/security
make openapi Dump openapi.json + regenerate web/types/api.ts
make migrate-create Interactive: generate a new Alembic revision
make reset Drop volumes, re-migrate, re-seed
make logs Tail Docker Compose logs

Gating test: tests/integration/test_multi_tenant_isolation.py must pass before every deploy. It verifies that a Firm A user cannot read Firm B prospects (gets 404, not 403 — no existence leak).


Configuration

All config is read from .env via Pydantic Settings (app/config.py). See .env.example for the full reference.

Key cost-control settings that affect the AI pipeline at runtime:

Variable Default Effect
ENRICHMENT_HARD_CAP_USD_CENTS 100 (= $1.00) Per-enrichment hard cap; pipeline aborts if exceeded mid-run
FIRM_DAILY_CAP_USD_CENTS 5000 (= $50.00) Per-firm daily cap; pipeline won't start if today's spend is at or above this

Both caps surface a clear error in the UI when hit.


Security & multi-tenancy

Row-level security. Every tenant-scoped table carries firm_id NOT NULL and a Postgres RLS policy (USING (firm_id = current_setting('app.current_firm_id', true)::text)). The app/security/rls.py middleware sets this session variable per transaction; the multi-tenant isolation integration test enforces it gates deploys.

Field-level encryption. Sensitive PII extracted from documents (account numbers, tax-return line items) is encrypted at rest using Fernet (app/security/encryption.py). Non-sensitive PII (name, email) uses transparent database encryption only, keeping it indexable.

Magic links. Only sha256(raw_token) is stored in magic_links.token_hash — the plaintext token is never persisted. First use captures IP and user-agent to the audit log. Links expire after 14 days (advisor-configurable 1–30 days) and are revocable from the advisor UI.

Invite-only auth. There is no open sign-up. Admins invite users via POST /api/v1/users/invite; the invitee receives a signed JWT token by email and sets their password at POST /api/v1/auth/accept-invite.


Further reading

  • PLAN.md — the original deep-research report and M1–M6 planning document that seeded this codebase. Preserved as-authored, useful context for understanding why decisions were made.
  • CLAUDE.md — command reference and architecture notes for AI coding agents working in this repo.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors