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.
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.
- 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.
To regenerate these screenshots: run
make reset && make dev, then in a second terminalcd web && npm run docs:screenshots. RequiresANTHROPIC_API_KEYandEXA_API_KEYin.env.
Drop in a name and a few hints. Discovery handles the research.
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:
Discovery questions — Framework-tagged questions (Kitces CLEAR, Kinder EVOKE, archetype probes) with rationales — exactly what to ask:
Anti-goals — Prompts that surface what the prospect wants to avoid, with guidance on raising them without triggering defensiveness:
Framing tips — Reframing language and landmines to avoid, tuned to the prospect's likely emotional context:
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):
Open-ended question (financial goals, career background):
Scale question (importance of legacy, charitable giving):
Multi-select question (top concerns, upcoming life transitions):
Optional document upload (tax return, brokerage statement):
All done — responses sent to the advisor:
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.
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
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
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])
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.
| 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 |
| 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 |
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)
- Python 3.14 (managed by
uv) uv—curl -LsSf https://astral.sh/uv/install.sh | sh- Node 20+
- Docker (for Postgres, Redis, MinIO, Mailhog)
git clone <repo-url> && cd discovery
cp .env.example .envEdit .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.
make deps # uv sync --all-extras + cd web && npm installmake up # starts Postgres, Redis, MinIO, Mailhog
make migrate # applies Alembic migrationsuv run python scripts/seed_dev.py # creates a dev firm + admin usermake 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 | 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 -d → http://localhost:8080 |
| 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).
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.
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.










