Stripe marketplace hold
@slashclone/stripe-marketplace-hold
Build a marketplace on Stripe Connect where your platform is the seller and holds each creator's share before paying it out, like eBay: Accounts v2 recipients, subscriptions and one-time sales, thin events, a release job, and refunds that reverse payouts. Writes the code and tests for you to review; never touches live keys.
SkillDevOpsv0.1.0 (latest)
- Activity
- New: be the first to try it
- Installs
- 0 installs
- Likes
- 0 likes
- Clones
- 0 clones
- 1---
- 2name: stripe-marketplace-hold
- 3description: Build a creator marketplace on Stripe Connect where the platform is the seller (merchant of record), takes a cut, and holds each creator's share for a set number of days before transferring it. Use when someone is adding paid content, subscriptions to creators, tips, or payouts to sellers.
- 4---
- 6# A Stripe Connect marketplace with an eBay-style hold
- 8Goal: buyers pay your platform, your platform keeps its fee, and each creator's share waits in a hold (for example 14 days) before it is transferred to them. Refunds and chargebacks inside the hold come out of money you still have.
- 10## Decide first (ask the person, do not guess)
- 121. **Who is the seller?** This skill assumes the platform is (separate charges and transfers). If creators must be the seller, use direct charges instead and skip the hold; tell them the tax and liability trade-off and stop.
- 132. **What is sold:** a monthly subscription to a creator, one-time items, tips, or all three. Who sets prices, and the platform's percentage.
- 143. **Hold length:** a number of days, kept in one setting (an environment variable with a default), between 0 and 90.
- 154. **Test mode only** until the business, terms, and tax are settled. Add a second flag that must be set before a live key does anything.
- 17## Accounts (Stripe Accounts v2)
- 19New Connect platforms cannot create Accounts v1 accounts. Use `POST /v2/core/accounts` with a pinned `Stripe-Version` header and a JSON body:
- 21- `configuration.recipient.capabilities.stripe_balance.stripe_transfers.requested: true` (the creator receives transfers and is not the seller);
- 22- `identity.country` (required; ask the creator, do not assume);
- 23- `dashboard: "express"`, which requires `defaults.responsibilities.fees_collector` and `losses_collector` both `"application"`. That makes the platform liable for negative balances; the hold is what protects you.
- 25Onboard with `POST /v2/core/account_links` (`use_case.type: "account_onboarding"`, refresh and return URLs). Do not send `configurations` in `account_onboarding`; current versions reject it. A creator is ready when `stripe_transfers.status` is `active`; read it fresh with `GET /v2/core/accounts/{id}?include=configuration.recipient`.
- 27## Events
- 29- Account changes arrive as **thin** events (`v2.core.account.updated`, `...[configuration.recipient].capability_status_updated`, `...[requirements].updated`) on a destination scoped to "Your account" with the thin payload style. They carry ids only: fetch the account. Also re-read it when a creator returns from onboarding, before any event arrives.
- 30- Payments arrive as snapshot events: `checkout.session.completed`, `invoice.paid` (each subscription month), `customer.subscription.updated` and `.deleted`, `charge.refunded`, `charge.dispute.created` and `.closed`.
- 31- Verify the `Stripe-Signature` header (HMAC-SHA256 of `timestamp.body`, 5-minute tolerance) against each destination's secret, and apply each event once by its id.
- 32- On newer API versions an invoice may not name its payment intent, and a subscription event may omit the period end. Read them from the API when missing.
- 34## Money flow
- 361. Checkout: charge the full amount on the platform. Do not set `transfer_data` or an application fee. Put your record id in `metadata` (and `subscription_data.metadata` for subscriptions) and as the `transfer_group`.
- 372. On payment: record the amount, your fee, and `release_at = paid_at + hold days`. Open access only on the webhook, never on the redirect back.
- 383. A daily job sends each creator's share once `release_at` has passed: `POST /v1/transfers` with `destination`, `source_transaction` (the charge, when you know it), and an **idempotency key per payment**, so a retried job never pays twice. Skip creators who are not ready or whom staff froze.
- 394. Refund or dispute: mark the payment, close access, and if the share was already transferred, reverse it (`POST /v1/transfers/{id}/reversals`). A won dispute restores access and lets the job pay again.
- 41## Steps
- 431. Read the existing billing code and data model. List what you will add before writing anything.
- 442. Add tables for creator accounts, purchases (with the version bought, if items are versioned), subscriptions, subscription payments, and tips. Every payment row gets `release_at`, a transfer id, and a transferred-at time.
- 453. Write the Stripe calls behind one small interface so tests use a fake, and keep the charge shape in one function.
- 464. Write the webhook handling, the release job, and staff refund and freeze actions.
- 475. Write tests against a fake Stripe: the fee split, access only after payment, the hold, one transfer per payment, reversal after a refund, and a frozen creator staying unpaid.
- 486. Document the setup steps for the dashboard: Connect business model, two webhook destinations, which events, and which secrets go where. List secret names only.
- 50## Rules
- 52- Never print, log, or ask for a key or webhook secret. Tell the person which environment variable to set.
- 53- Never store card, bank, or identity details. Keep Stripe ids, amounts, and statuses only.
- 54- A live key alone must not enable payments.
- 55- Buyers accept your terms at checkout; record which version of the terms.

