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.
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.
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.
Next.js 16 · React 19 · Tailwind 4 · shadcn · TanStack
PostgreSQL · Prisma · 54 models · 29 migrations
Stripe Connect Express · Checkout · Radar · Identity
S3 presigned + SSE-KMS · Cloudinary · signed local dev
DigitalOcean · nginx · PM2 · symlink releases · GitLab CI
A marketplace, an escrow,
and a compliance desk
Every listing and every seller is reviewed before it goes live, with structured rejection reasons.
Idea detail is redacted for non-purchasers; files unlock only after payment clears.
Buyer offers, seller approval, and an atomic reservation with a timeout so two buyers can never win the same idea.
Funds held on the platform balance, released by buyer confirmation, window expiry or admin action.
Trial, Pro and Business plans synced to Stripe products, each with its own listing cap and commission rate.
Identity tiers by amount, sanctions screening, risk scoring, velocity limits, disputes and chargeback debt.
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.
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.
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.
Make the wrong thing
impossible
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.
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.
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.
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.
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.