Commerce.
Support and unlocks should strengthen the Profile World, not turn Casset into a checkout page.
Commerce is a support layer around audiovisual identity. A fan collects because a Hook Object, Listening Room, or Release Ritual made the moment feel worth keeping. Under the hood Casset uses Stripe Connect Destination charges: the platform is the merchant of record, the artist is the connected account, and the webhook finishes entitlement, activity, referral, and payout state.
The happy path
- Fan lands in a
Profile World, plays the Hook Object, and taps Collect when they want to support/unlock more. Client callsPOST /api/checkout/payment-intentwith the artist slug + referral code (if any). - Server creates a Stripe
PaymentIntentwith the artist's connected account astransfer_data.destinationand anapplication_fee_amountfor the platform cut. Client secret is returned. - Client mounts Stripe's Payment Request (Apple Pay / Google Pay). If the device can pay, the native sheet appears; otherwise we fall back to a standard card element.
- On confirm, Stripe captures the payment and fires
payment_intent.succeededat/api/webhooks/stripe. The webhook is the source of truth — the client never grants entitlement on its own. - Webhook handler:
- Inserts a
Purchaserow (idempotent on the PI id). - Emits an
ActivityEventso followers and the artist see "@fan collected" in realtime. - If a
ReferralAttributioncookie is present, writes the referral so the referrer accrues credit. - Nudges any
Notificationrows so the artist sees the collect in their bell.
- Inserts a
- Stripe disburses the destination transfer on the artist's configured payout schedule (default: daily rolling).
Why Apple Pay first
The single most predictive signal for collect conversion on mobile is whether the purchase is one tap. Casset leans on Apple Pay / Google Pay because they're:
- Already authenticated (Face ID), so there's no card form, no email step, no ZIP lookup.
- Trustworthy at a glance — the sheet is OS-chrome, not our chrome.
- Supported inside TikTok / Instagram in-app browsers, which is where half of inbound traffic lands.
Card fallback exists, but the default success path never requires typing.
Entitlement, not tokens
Casset doesn't sell tokens or subscriptions. A Purchase row is a permanent entitlement record. Anywhere the product asks "can this user hear the full track?", the answer comes from:
SELECT 1
FROM Purchase
WHERE userId = <session.userId>
AND artistId = <track.artistId>
AND state = 'granted'
LIMIT 1The collect is one-time, the unlock is forever. That's also why every audio byte goes through lib/audio-access.ts — the entitlement check happens on every stream request, not once at login.
Referrals
Every share produces a unique URL via POST /api/casset/[slug]/referral-link. Clicking it sets an attribution cookie (ref=fromUserId) that persists for 30 days. If the visitor collects within the window, we write a ReferralAttribution row tying that Purchase to the referrer.
Referral rewards are currently credit-based, tracked in CreditLedger (append-only). In the studio, referrers see a "you're credited" counter; when rewards convert to cash, payout is merged into the next scheduled transfer.
Per-track rewards
Rewards are the lightweight, per-hook bounty mechanic. On any track, owners can set:
rewardAmount— integer cents.rewardCurrency— ISO 4217, defaultUSD.rewardDescription— the offer ("10 fans who share get the demo").rewardActive— bool; lets you draft + flip on later.
When a fan posts a qualifying hook video (attached via POST /api/hooks/submissions), the artist reviews the submissions in /clips. Approved submissions enter the payout pipeline.
Release quests / campaigns
Campaigns are useful only when they make a Release Ritual more participatory. The implemented system supports structured prize pools: creators fund a campaign, fans submit hook videos, and winners split the pool by a scoring algorithm (views × engagement × integrity). Product language should frame this as release energy, not promoter software.
POST /api/campaign/[id]/fund— creator funds the pool; Stripe holds the money until the campaign ends.POST /api/campaign/[id]/launch— opens the campaign for submissions.lib/scoring/runs periodic snapshots.POST /api/campaign/[id]/select-winner— closes the drop, snapshots final scores, and queues payouts.POST /api/campaign/[id]/payout— releases the prize pool via Stripe Connect transfers.
Fees + economics
Headline split (today):
- Stripe: ~2.9% + $0.30 processing
- Casset platform fee: 10%
- Artist: everything else
Platform fee may change; this page is informational, not a commercial commitment. Check your Studio dashboard for your effective rate.
Refunds + disputes
Refunds come through the same webhook as charges but on charge.refunded. The handler:
- Transitions the
Purchase.statefromgrantedtorefundedso entitlement queries stop returning it. - Writes a reversing entry in
CreditLedgerso the artist's earned-balance updates without mutating the original record. - Cancels any pending referral rewards tied to the purchase.
Chargebacks follow the same flow but originate from charge.dispute.created. The artist is notified and can submit evidence through Stripe; we don't auto-accept.
Onboarding the artist
Before a casset can accept payments, the owning user needs a connected Stripe account. We use POST /api/studio/stripe/connect → embedded Stripe onboarding flow → POST /api/studio/stripe/webhook handles the account-updated events. The artist record tracks two flags:
stripeAccountId— theacct_id on Stripe.stripeOnboardingComplete— set when Stripe reportscharges_enabled && payouts_enabled.
The UI gates the Collect button on stripeOnboardingComplete — no button, no money in flight, no failed charges.
Stripe webhooks, summarized
payment_intent.succeeded— grant Purchase, emit activity, resolve referrals.charge.refunded— revoke entitlement, reverse credits.charge.dispute.created— notify artist, freeze related payouts.payout.paid— markCampaignPayout/PerformancePayoutas settled.account.updated— syncstripeOnboardingCompleteflag.
Full routing table in docs/stripe-webhooks.md.