Deal-desk CRM
The CRM behind a deal desk for closeout and surplus lots, built so buyers and sellers never see each other.
- Built for
- A deal desk for closeout and surplus lots
- My part
- Designed and built it
- Status
- Live
- Stack
- Next.js, TypeScript, Postgres on Supabase, Fly.io, Vercel


The desk is set up to broker closeout and surplus lots between sellers and buyers. The CRM is built to take a lot from a seller's chat message to a settled deal: priced against resale history, verified, offered blind to matched buyers, paid for and delivered. I designed and built it. Its web app is live since Jul 2026, and its WhatsApp intake is built, not live.
The size of the build
- tests
- over 950
- commits
- over 100
Kept blind
Offers, invoices and delivery papers never name the other party: they are built only from snapshots whose queries never touch it, and a fuzz suite tries to make them leak.
Blind is the default. A freight term that would hand the buyer's carrier the seller's pickup address holds the deal until a partner clears it or waives it on the record, and a delivery that is not blind carries a red warning.
The buyer list for an offer leaves out the seller, and any other name that seller trades under, before any scoring, and every exclusion records its reason.
Human gates
A lot can be offered to buyers only after a partner approves it. At any stage, an outbound payment cannot be recorded without a partner's approval, and neither can taking title to a lot. The approvals are rows the CRM checks, and editing a deal's items revokes them.
Sending is switched off in code: the composer only saves drafts, the page has no send button, and turning sending on takes a database migration. A kill switch in the settings stops sending at once, with no deploy.
Money
Cash and accounting are kept apart: payments record the wires that moved, and a ledger holds every cost and sale as a signed line, all in whole cents. A deal's final margin is written once, at settlement, and settling refuses while money is still in flight.
Intake
The WhatsApp intake is built, not live: a listen-only bridge to record each thread, Claude to draft the deal from the seller's message, and a single transaction that records, links and queues each message, so none is stranded.
Pricing is built so that, within the same pricing run, a model's estimate never overwrites a price backed by resale history, and a partner's price wins over both.
The buyer portal's database role holds no table rights at all, so each action a buyer takes goes back through the desk's own writers.
Tested
An acceptance walk takes a deal from intake to settled on an empty database: the gates refuse and then pass, the seller is left off the offer, the invoice names no seller, and no money is left in flight at the end.
Every status change goes through the desk's own writers and is logged, and the test gate applies every migration to an empty Postgres, so a migration that cannot build from nothing fails.
Contact
For a role, a project or a question about anything here, email me.
An AI phone line that answers questions about my work.
- Call my AI
- (213) 635-1440
- Download
- Résumé (PDF)