(01) — FOUNDER + LEAD ENGINEER / YIELDSTREAM LLC / 2025—PRESENT
An underwriting intelligence platform that replaces gut-feel lender routing with a three-layer scoring model, document parsing, and outcome-based learning.
Merchant cash advance brokers operate in a market that moves on instinct. A merchant submits a funding application. The broker glances at a bank statement, eyeballs the daily balance, and routes the deal to whichever lender they have the strongest relationship with. If that lender declines, the broker tries the next name on their mental list. Repeat until someone bites or the deal dies.
The numbers tell the story. Industry pull-through rates — the percentage of submissions that actually fund — hover between 15% and 25%. A broker doing $5M/month in submissions at a 20% pull-through is leaving $750K+ in potential funded volume on the table compared to a 35% pull-through. On the lender side, underwriters are drowning in misrouted deals that never should have reached their desk.
Existing tools don't address this. Generic CRMs like Salesforce and HubSpot are agnostic to the MCA workflow. They can track that a deal was submitted, but they can't tell you which lender is most likely to fund it. The purpose-built MCA platforms that do exist are largely 2010s-era PHP applications — functional for status tracking, useless for decision support. Nobody has built a scoring layer that actually predicts which lender will fund a given merchant profile.
The founding insight was that this is fundamentally a data problem, not a workflow problem. Every funded deal generates signal: which lender funded it, what the merchant's financials looked like, how long it took, what terms were offered. That signal is sitting in spreadsheets and email threads. If you could structure it and score against it, you could replace gut feel with informed recommendations.
I built YieldStream solo, which meant every architectural decision had to be defensible by one person on call. There was no second engineer to debug a 3 AM Kubernetes issue, so the infrastructure had to be operationally simple.
No external funding. The cost ceiling was real: hosting, database, AI inference, document processing — all had to run cheaply enough that I wasn't burning personal savings to keep the lights on during pre-revenue development.
The MCA industry is opaque by design. There are no public lender APIs. No standardized submission format. Lender appetite criteria live in PDF rate sheets, email threads, and phone calls. Bank statements — the primary underwriting document — arrive in 14+ distinct formats with no consistent structure. And all of this data is PII-laden financial information subject to multi-state regulatory requirements.
A. Three-layer scoring model
The obvious approach would be a single ML model trained on historical deal outcomes. I rejected it for two reasons: interpretability and the cold-start problem. On day one, I had zero historical funded-deal data. A black-box model would produce numbers nobody trusted. I needed a system that could produce defensible scores from rules-based signals first, then refine with data as outcomes accumulated.
The three layers, each scored 0-100 and weighted into a composite:
B. Document intelligence
Before scoring could work, I needed structured data from unstructured bank statements. I built a multi-tier parsing pipeline: bank-specific templates first for highest accuracy (Chase, BofA, Wells Fargo, and 9 others), generic structural extraction as a middle tier, and LLM-based extraction as the final fallback. This was later extracted into Ledger as a standalone service.
C. Outcome-based learning
When a deal funds or gets declined, the system captures a snapshot: the merchant's profile at submission time, the bank intelligence at that moment, and the prediction that was made. It records the actual outcome alongside the predicted probability and calculates variance. Declines trigger temporary score penalties on the responsible lender — categorized by reason (credit quality, NSF, stacking limit) — that auto-expire after 30 days. The system gets sharper with every closed deal.
Next.js 15 with App Routerfor the frontend and API layer. Not because it was trendy — because server actions + React Server Components let me colocate data fetching with UI without maintaining a separate API service. For a solo developer, that's one fewer deployment to manage, one fewer repo to maintain.
Supabase (PostgreSQL) as the database, auth provider, file storage, and realtime layer. The key architectural decision was using Row-Level Security for multi-tenant isolation. Every table is scoped by org_id, enforced at the database level via a get_my_org_id() SQL function. No ORM — direct Supabase client calls with RLS doing the heavy lifting. This means even if application code has a bug, tenant data can't leak across organizations.
The scoring engine is pure TypeScript. No ML libraries, no model files. The three layers compute independently and blend via weighted sum:
const SCORING_WEIGHTS = {
global: 0.25,
relationship: 0.5,
attribute: 0.25,
};
const RELATIONSHIP_MULTIPLIER = 1.15; // +15% for strong pull-through
const PREDICTION_TTL_HOURS = 24;
const MAX_REASONING = 3; // Gemini calls limited to top 3 lendersAttribute scoring applies a base of 50 and adjusts by factor. Revenue at 2x the lender's minimum adds +20. FICO above 700 adds +15. More than 5 NSFs in 30 days penalizes -25. The scoring is deterministic and auditable — every point can be traced to a specific signal.
Gemini 2.0 Flash Lite handles two tasks: bank statement analysis (extracting 20+ financial metrics from OCR'd text) and lender reasoning (generating natural-language explanations for the top 3 recommendations). I call Gemini only for the top 3 scored lenders, not all of them — a deliberate cost optimization on the free tier's 15 RPM limit.
Inngest orchestrates all background work: document parsing, AI enrichment, prediction generation, outcome recording, decline intelligence, renewal alerts, and credit resets. Each function is a discrete, retryable step function — if Gemini rate-limits, the enrichment job retries without re-running the parsing step.
The document parser is a Python FastAPI microservice with a 5-tier extraction cascade: pdfplumber handles 90% of text-based PDFs, PyMuPDF catches encrypted files, Tesseract OCR processes scanned images, LlamaParse serves as an intermediate fallback, and Gemini AI is the final safety net. Twelve bank-specific templates (Chase, BofA, Wells Fargo, TD, PNC, and 7 others) provide high-accuracy extraction before the generic fallback fires. When an unrecognized bank accumulates 5+ processed statements, the system automatically flags it for template creation.
Killing LlamaParse
The initial document pipeline relied on LlamaParse, a cloud-hosted PDF extraction service. It worked — until it didn't. Latency averaged 60 seconds per document. The API cost scaled linearly with volume. And critically, I had no control over extraction quality or the ability to build bank-specific parsing rules.
I replaced it with a local Python FastAPI service in a single sprint. pdfplumber alone handled 90% of incoming PDFs. Adding bank-specific templates (starting with Chase and BofA, the two highest-volume banks) pushed extraction accuracy above what LlamaParse was producing. The cascade fallback meant no single tier's failure could block the pipeline. Processing time dropped from 60 seconds to under 2 seconds for template-matched documents.
The IDOR that almost shipped
During a security audit of my own API routes, I found that the prediction accuracy endpoint accepted org_idas a query parameter. Any authenticated user could pass a different org's ID and read their prediction data. The fix was straightforward — derive org_id from the authenticated profile, never from the request — but it exposed a broader pattern. I spent the next week wrapping every mutation endpoint in a secureApiHandler utility and adding requireRole checks to every server action. The lesson: RLS protects the database layer, but API routes need their own authorization gate.
The global pool cold-start
The global score layer depends on anonymized cross-org outcomes. With zero organizations and zero outcomes, the entire layer returned 50 (the fallback) for every lender. Relationship scores were similarly empty. In practice, early predictions were driven almost entirely by the attribute score — pure buy-box matching. I added a confidence_level field and a short TTL (5 minutes) for fallback-dominated predictions, so the UI could surface a clear signal: "these scores will improve as you record outcomes." The system was honest about its own uncertainty. [TK: specific feedback from early pilot users on how this affected trust]
YieldStream is in production on Vercel, pre-revenue. The platform is feature-complete for the core workflow: merchant intake, document parsing with AI enrichment, three-layer lender scoring with Gemini reasoning, submission tracking through to funded deal, and outcome-based model refinement.
The system processes bank statements through 12 bank-specific templates with a 5-tier extraction cascade. It scores merchants against lender buy boxes using deterministic rules, augmented by Gemini-generated reasoning for the top 3 matches. Every funded or declined deal feeds back into the scoring model through the outcome recording pipeline.
What I built alone in months would have been a team effort at a funded startup. The codebase is a single Next.js monolith with one Python microservice, zero Kubernetes, zero Redis — and it handles the full lifecycle from document OCR to lender recommendation to portfolio analytics.
Six months into building YieldStream, I realized the document parsing pipeline had become the most reusable piece of the system. Bank statement parsing isn't unique to MCA — any fintech product that underwrites against bank data needs the same capability. Keeping it coupled to YieldStream's codebase meant it could only serve one product.
I extracted the parser into Ledger, a standalone document intelligence service with its own API, its own test suite (62 tests across 6 suites), and its own deployment. The separation was architectural, not cosmetic:
This wasn't premature abstraction. It was platform thinking — structuring a product family instead of building features.
The immediate roadmap: onboard the first pilot brokers and accumulate real outcome data. The scoring model is designed to improve with volume, but that improvement is theoretical until real deals flow through it.
On the technical side, I'm watching three areas. First, the in-memory rate limiter needs to move to Redis before horizontal scaling. Second, the analytics layer is read-only and dashboard-scoped — a proper BI integration (or materialized views for complex aggregations) would serve power users better. Third, I chose not to build a mobile app. Brokers work from their desks. If that assumption proves wrong, the API-first architecture makes a mobile client straightforward to add.