(01) — FOUNDER + LEAD ENGINEER / YIELDSTREAM LLC / 2025—PRESENT

YieldStream

An underwriting intelligence platform that replaces gut-feel lender routing with a three-layer scoring model, document parsing, and outcome-based learning.

Next.js 15SupabaseTypeScriptGemini AIPythonInngest
In production2025
01

The problem

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.

02

The constraints

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.

03

The approach

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:

  • Global score (25%) — Market signal. Aggregates anonymized funding outcomes from all organizations with time-decay weighting. Deals from the last 30 days get full weight; 90+ days get 0.2x. Answers: is this lender actively funding deals like this?
  • Relationship score (50%)— Pull-through rate. The broker's own history with each lender, time-weighted. This is the primary differentiator — weighted at 2x the other layers because a broker's specific relationship with a lender is the strongest predictive signal. A +15% bonus kicks in when pull-through exceeds 60% with 3+ funded deals.
  • Attribute score (25%)— Risk match. Does the merchant's financial profile (FICO, revenue, NSF count, daily balance, position count) fit the lender's stated buy box? Hard disqualification rules force the score to zero if the merchant violates a lender's restricted states, industries, or minimum thresholds.

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.

Three-layer scoring architecture
MERCHANTPROFILEGLOBAL SCOREMarket signals / 25%Cross-org outcomes, time-decayRELATIONSHIP SCOREPull-through rate / 50%ISO-specific deal historyATTRIBUTE SCORERisk match / 25%Financials vs. buy boxCOMPOSITEWeighted blendTOP 3 LENDERSGemini reasoningEstimated termsRisk factors
04

The build

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:

lenderMatcher.tsTypeScript
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 lenders

Attribute 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.

Document intelligence pipeline
UPLOADVault storage
CLASSIFY11 doc types
EXTRACT5-tier cascade
ENRICHGemini AI
SCORE3-layer model
Extraction cascade (Tier 1 → Tier 5)
01pdfplumber90% of PDFs
02PyMuPDFEncrypted PDFs
03Tesseract OCRScanned images
04LlamaParseFree tier fallback
05GeminiFinal fallback

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.

05

The hard parts

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]

06

The outcome

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.

19+Database tables40+ indexes, full RLS
12Bank templatesAuto-flagging for new banks
13Background jobsInngest step functions
62Parser testsAcross 6 test suites
07

Extracting Ledger

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:

  • Separation of concerns. YieldStream is the CRM and intelligence layer. Ledger is the data extraction layer. Each can evolve independently.
  • Independent scaling. Document parsing is bursty and CPU-heavy (OCR, PDF manipulation). Scoring is steady and database-heavy. Different scaling profiles need different infrastructure.
  • Reusability. Ledger can serve other products — or be offered as a standalone API to other fintechs that need bank statement extraction.
  • API-first design.Forcing communication through an API contract eliminated implicit coupling. If Ledger's internal parsing logic changes, YieldStream doesn't need to redeploy.

This wasn't premature abstraction. It was platform thinking — structuring a product family instead of building features.

08

What's next

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.