Case study — IdeaKoi

A marketplace where ideas become assets.

IdeaKoi lets founders sell validated startup ideas — decks, business plans, research — to buyers who sign an NDA, negotiate an offer and pay through Stripe. Files unlock only after payment, every listing is reviewed by a human, and the platform earns a commission that depends on the seller's plan. Live at ideakoi.com.

Data model54Prisma models, 37 enums
Surface60 / 159Pages / API route handlers
Tests194Vitest files, incl. static guards
TeamOne~99% of 940+ commits
Timeline10 moDec 2025 → Sep 2026, live
01 — The problem

An idea is worthless the moment you describe it — unless the platform protects both sides.

Sellers won't list a real concept if a buyer can read the whole thing and walk away. Buyers won't pay for a concept they can't evaluate, or one that turns out to be fabricated. Every existing option picked a side.

Underneath the product problem sat a money problem: holding funds between strangers, splitting commission by plan, handling disputes and chargebacks, and proving to anyone who asked exactly where every cent went.

02 — Role & stack

Solo engineer,
inherited codebase

Took over a template-derived repo carrying a dead CRM, a blog module and a second marketplace, cut it back to the product, then built the money and compliance layers from scratch.

MarketplacePublic browse, filter and search over admin-reviewed idea listings, with content redacted until purchase.
Seller panelListing CRUD with a rich-text editor, presigned uploads, subscription-enforced limits and Stripe Express payouts.
Buyer panelOffers, purchases, saved ideas, tiered identity verification and signed short-lived downloads.
Admin consoleModeration queues, seller KYC, commissions, plans, disputes, fraud, ledger and seller-debt views.
Frontend

Next.js 16 · React 19 · Tailwind 4 · shadcn · TanStack

Data

PostgreSQL · Prisma · 54 models · 29 migrations

Payments

Stripe Connect Express · Checkout · Radar · Identity

Storage

S3 presigned + SSE-KMS · Cloudinary · signed local dev

Infra

DigitalOcean · nginx · PM2 · symlink releases · GitLab CI

03 — What I built

A marketplace, an escrow,
and a compliance desk

Admin-reviewed listings

Every listing and every seller is reviewed before it goes live, with structured rejection reasons.

NDA-gated access

Idea detail is redacted for non-purchasers; files unlock only after payment clears.

Offer negotiation

Buyer offers, seller approval, and an atomic reservation with a timeout so two buyers can never win the same idea.

Escrow & release

Funds held on the platform balance, released by buyer confirmation, window expiry or admin action.

Tiered subscriptions

Trial, Pro and Business plans synced to Stripe products, each with its own listing cap and commission rate.

Trust & safety

Identity tiers by amount, sanctions screening, risk scoring, velocity limits, disputes and chargeback debt.

04 — Deep dive

Money that can prove itself

A buyer's offer is approved, the idea is reserved atomically, and Stripe Connect charges the buyer with the commission routed to the platform in the same call. From there the funds sit in escrow until a release rule fires.

  • Destination charges split commission automatically at checkout.
  • Webhooks are the only writer of a paid state — signature-verified, idempotent, logged.
  • Every amount is an integer in cents; no floating-point money anywhere.
  • Ledger batches must balance, enforced by a deferred database trigger.
TrialFree3 listings · 15% commission
Pro$29 / mo25 listings · 10% · 5 featured
Business$99 / moUnlimited · 5% · 20 featured
05 — Trust & safety

Strangers, money,
and no excuses

A marketplace that moves real money between people who have never met inherits every obligation of a payments company. This is the layer most side projects skip, and the one that took the longest.

Six controls, one lifecycle
01IdentityStripe Identity verification, tiered by purchase amount.
02ScreeningSanctions and PEP screening behind a provider interface, re-run on a schedule.
03RiskRadar rules, 3DS, velocity limits, blocklists and early-fraud-warning automation.
04DisputesTwo-sided evidence, admin decisions, and a dispute-timeout job.
05ChargebacksTransfer reversal and a seller-debt ledger when money has already moved.
06ReconcileDouble-entry ledger reconciled against Stripe daily.

Identity checks, screening and risk scoring were built as pluggable providers with stubs, so the product could ship and the vendors could be swapped. A few of these phases are still closing out their verification gaps — I'd rather say that than call them finished.

06 — Engineering decisions

Make the wrong thing
impossible

01
An append-only double-entry ledgerMoney

Ledger rows are immutable at the database level — a trigger refuses updates and a deferred constraint refuses any batch that doesn't balance. One module is the sole writer, and a static test fails the build if anything else imports the write path.

Why — When a marketplace holds other people's money, the database should make the wrong thing impossible, not just unlikely.

02
Webhooks are the source of truthPayments

An earlier build let the client confirm payment. I deleted that path and made the signature-verified Stripe webhook the only thing that can mark a transaction paid, with an event table for idempotency.

Why — A payment state that can be set from a browser is a payment state that can be forged.

03
Completed is two states, not oneEscrow

Split a single COMPLETED status into buyer-paid and seller-paid, held the funds on the platform balance, and released through explicit triggers run by a cron-secret protected job.

Why — Release rules change. Modeling them as a policy, not a flag, made buyer confirmation, expiry and admin override the same code path.

04
Two auth systems collapsed into oneAuth

The inherited codebase had a cookie session and a separate bearer-JWT path that had drifted. I unified them behind one requireApiAuth / requireApiRole helper and added a static coverage check that proves every mutating route is guarded.

Why — 159 route handlers is too many to audit by hand more than once.

05
Never call a provider inside a webhookCompliance

Sanctions screening writes to an outbox inside the transaction; a sweep job makes the external call afterwards and records the result.

Why — A slow third party must never hold a Stripe webhook open or roll back a payment that already happened.

07 — Scale

Roughly 114,000 lines of TypeScript, most of it about money.

54 models · 37 enumsOne schema spanning listings, offers, escrow, subscriptions, disputes, compliance and the ledger.
159 API routes · 60 pagesThree role panels plus the public marketplace, all under one guarded auth path.
5 scheduled jobsEscrow auto-release, dispute timeouts, reservation timeouts, sanctions screening and re-screening.
17 phases · 2 milestonesSpec-driven delivery with a plan and a verification document per phase, some compliance phases still in progress.

Built so the ledger never lies.