Inspiration

Wholesale businesses get squeezed from both directions at once: cash locked up in warehouse inventory, and cash locked up in customer payment terms (net 30/60/90). We kept hearing the same story from small wholesalers — they're profitable on paper, but they still can't make payroll on a random Tuesday in week six. Most of them manage this with a gut feeling and a spreadsheet that's three weeks stale. We wanted to build something that could say, concretely: "On day 45, you'll be short $23,000 — here's why, and here's whether you should liquidate inventory, chase a customer, or take a loan."

What it does

Wholesale Cash Flow Intelligence pulls real SKU sales history, on-hand and in-progress inventory, outstanding invoices, and vendor payment schedules into one dashboard that:

  • Projects a deterministic 90-day cash runway built straight from AR/AP data — no AI guesswork on every page load
  • Pinpoints exactly which slow-moving SKUs and overdue customers are causing the squeeze
  • Recommends whether to liquidate inventory, chase receivables, or take financing, using true APR-normalized comparisons instead of apples-to-oranges percentages

How we built it

We split the app into two layers on purpose. A deterministic projection engine (/api/projection) reads Aurora directly and walks the calendar day by day — no LLM call, no cost, instant on every page load:

$$ \text{balance}_t = \text{balance}_{t-1} + \text{cashIn}_t - \text{cashOut}_t $$

AR inflow dates are shifted forward by each customer's historical average days-late, and invoices already past that window are excluded from the curve entirely and surfaced separately as an "Overdue AR" callout with a one-click "what if collected today?" simulation. We later added a symmetric what-if: planned capital injections (a future loan draw or equity deposit) feed into the same extraInflows mechanism, so both "more cash coming in sooner" and "more cash arriving from outside" share one code path instead of two.

On top of that sits a layer of Claude tool-use agents — one each for cash flow forecasting, dead-stock tiering, collections priority, payables, purchasing, and financing — that all return structured JSON instead of free-form text, so the dashboard never has to parse prose into numbers.

For inventory health, days-of-supply is:

$$ \text{daysOfSupply} = \frac{\text{qtyOnHand} + \text{qtyWip}}{\text{avgDailyVelocity}} $$

and SKUs get ranked into A/B/C/D tiers by a composite score (sales velocity, margin quality, excess days of supply, return/obsolescence risk) — deterministic and auditable, not an AI judgment call — which then drives both the suggested liquidation discount per tier and, on the financing side, a tier-weighted liquidation calculation that consumes the cheapest (highest-quality) inventory first to cover a cash gap, rather than pricing the whole liquidation off one flat haircut.

The financing comparison itself normalizes everything to a common APR:

$$ \text{APR} = \frac{\text{cost}}{\text{raised}} \times \frac{365}{\text{days}} $$

applied only to true time-priced instruments (bank loan, AR financing) — inventory liquidation is shown as a one-time haircut instead of an annualized rate, because annualizing a 30-day, 33% haircut produces a meaningless ~240% "APR" that would make liquidation look catastrophically worse than it is.

Data flows CSV → DynamoDB (raw, deduped) → Aurora (typed tables, drizzle-orm) → either the deterministic projection or a Claude agent, with a daily Vercel Cron keeping the saved AI forecast current. As real upload volume grew past a few hundred thousand rows, we went back and gave the data layer the indexing story it needed from day one: a type-uploadedAt GSI on the DynamoDB raw table so it can be queried by type without a full scan, a 90-day TTL so raw staging rows expire automatically instead of accumulating forever once they're normalized into Aurora, and explicit company_id (+sku where it's the join key) indexes on every tenant-scoped Postgres table instead of relying on sequential scans.

Challenges we ran into

  • WIP kept getting silently ignored. Early on, days-of-supply only counted on-hand stock, which made in-progress inventory invisible and inflated the reorder queue — a one-line fix ((qtyOnHand + qtyWip) / velocity) but it took real data to notice the queue was wrong.
  • ?? is not the null-coalescing operator you want when zero is valid data. A WIP value of legitimately 0 units was being treated the same as "no data," breaking a fallback chain in the ETL layer — a reminder that "falsy" and "missing" are not the same thing in financial data.
  • One flat liquidation discount lied about small gaps. Pricing all liquidation off a single blended haircut made even a tiny cash gap look expensive, when in reality it could be covered almost free by selling the highest-quality (A-tier) stock first. Moving to sequential, tier-ordered consumption fixed the economics but meant rethinking the calculation as a marginal-cost walk instead of one multiplication.
  • Telling cash apart from profit, visually. It's easy to compute the numbers; it's harder to make a chart that makes a profitable-but-cash-poor business legible at a glance. The dual-line cash-vs-accrual chart, where the vertical gap between the two lines equals working capital deployed in AR + inventory − AP, took a few iterations before it actually told the story instead of just plotting two series.
  • No index isn't a bug until it is. Every table started with nothing but a primary key, which is fine at demo scale and quietly becomes the first thing a judge — or a real customer's data volume — finds. Retrofitting company_id/sku indexes onto Aurora and a type-uploadedAt GSI plus TTL onto DynamoDB after the fact was straightforward, but it meant consciously revisiting "what will this look like at 10x the rows" instead of only optimizing for correctness.

What we learned

Deterministic and AI-driven logic should be architecturally separate, not blended — once the projection engine didn't need an LLM call to render, every page load became instant and free, and the AI layer could be reserved for genuinely judgment-heavy tasks like vendor negotiation copy. We also learned that financial dashboards live or die on getting the edge cases right: zero vs. null, marginal vs. average cost, annualized vs. one-time — these aren't cosmetic details, they're the difference between a recommendation a wholesaler can actually trust and one that quietly misleads them. And on the infrastructure side: a multi-tenant table without an index on the tenant key isn't a problem until your data volume makes it one — by the time we added them, DynamoDB already held 300k+ raw rows, which is exactly the kind of "it worked in the demo" gap we'd rather get asked about than get caught by.

Built With

  • amazonaurorapostgresql
  • amazondynamodb
  • anthropicclaudeapi
  • awsrdsdataapi
  • next.js
  • react
  • tailwindcss
  • typescript
  • vercel
Share this project:

Updates