.claude/skills/ridendine-payment-distribution/SKILL.md
Master RidenDine's multi-party payment distribution model. Use when: (1) implementing payment splits, (2) onboarding new payment recipients (Chef, CoCo, Driver, Delivery Company), (3) calculating commission splits, (4) debugging payout issues, (5) extending payment infrastructure. Key insight: Customer pays → Platform distributes to 4 parties with CoCo receiving 60/40 split on $10/order base.
npx skillsauth add Ritenoob/ridedine ridendine-payment-distributionInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
RidenDine is a multi-party marketplace where customer payments must be split between:
Use this skill when:
Per Order:
Customer Total: $X
├─ Food Cost: $Y (to Chef)
├─ CoCo Share: $10 × 60% = $6.00 (to CoCo)
├─ Platform Share: $10 × 40% = $4.00 (retained)
├─ Driver Fee: delivery_fee_cents (to Driver)
└─ Delivery Company Fee: TBD (if applicable)
CoCo Partnership Terms:
Location: backend/supabase/functions/create_checkout_session/index.ts
Current Flow:
const session = await stripe.checkout.sessions.create({
mode: 'payment',
payment_intent_data: {
application_fee_amount: order.platform_fee_cents, // Platform keeps this
transfer_data: {
destination: order.chefs.connect_account_id, // Chef gets (total - platform_fee)
},
},
// ...
});
Problem: This only handles Chef + Platform. No mechanism for CoCo, Driver, or Delivery Company.
Why not Checkout Session splits? Stripe Checkout only supports ONE destination account. For 4+ parties, use Transfers API after payment capture.
New Flow:
1. Customer pays via Stripe Checkout (100% to Platform account)
2. Webhook: payment_intent.succeeded
3. Calculate splits server-side
4. Create separate Transfers to each connected account:
- Transfer to Chef (food cost)
- Transfer to CoCo ($6.00)
- Transfer to Driver (delivery_fee_cents)
- Transfer to Delivery Company (if applicable)
5. Platform retains remainder ($4.00 from CoCo + any additional platform fee)
Step 1: Add Connected Accounts for All Parties
-- backend/supabase/migrations/YYYYMMDD_add_payment_recipients.sql
-- Add Stripe Connect account for CoCo (one account for entire organization)
CREATE TABLE IF NOT EXISTS coco_config (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
stripe_account_id TEXT UNIQUE,
payout_enabled BOOLEAN NOT NULL DEFAULT false,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ
);
-- Add Stripe Connect account per driver
ALTER TABLE drivers
ADD COLUMN IF NOT EXISTS connect_account_id TEXT UNIQUE,
ADD COLUMN IF NOT EXISTS payout_enabled BOOLEAN NOT NULL DEFAULT false;
-- Track individual transfers for reconciliation
CREATE TABLE IF NOT EXISTS payment_transfers (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
order_id UUID NOT NULL REFERENCES orders(id),
recipient_type TEXT NOT NULL CHECK (recipient_type IN ('chef', 'coco', 'driver', 'delivery_company')),
recipient_id UUID NOT NULL, -- profile_id or driver_id or coco_config.id
stripe_transfer_id TEXT UNIQUE,
amount_cents INT NOT NULL,
status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'succeeded', 'failed')),
failure_reason TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ
);
CREATE INDEX idx_payment_transfers_order ON payment_transfers(order_id);
CREATE INDEX idx_payment_transfers_recipient ON payment_transfers(recipient_type, recipient_id);
Step 2: Create Transfer Distribution Edge Function
Location: backend/supabase/functions/distribute_payment/index.ts (NEW FILE)
import Stripe from 'https://esm.sh/[email protected]?target=deno';
import { createClient } from 'https://esm.sh/@supabase/[email protected]';
const stripe = new Stripe(Deno.env.get('STRIPE_SECRET_KEY') || '', {
apiVersion: '2023-10-16',
httpClient: Stripe.createFetchHttpClient(),
});
const supabaseAdmin = createClient(
Deno.env.get('SUPABASE_URL') ?? '',
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') ?? ''
);
Deno.serve(async (req) => {
const { order_id, payment_intent_id } = await req.json();
// Fetch order with all related data
const { data: order, error: orderError } = await supabaseAdmin
.from('orders')
.select(`
*,
chefs!inner(connect_account_id),
deliveries!inner(driver_id, delivery_fee_cents, drivers!inner(connect_account_id))
`)
.eq('id', order_id)
.single();
if (orderError) throw orderError;
// Fetch CoCo config
const { data: coco } = await supabaseAdmin
.from('coco_config')
.select('stripe_account_id')
.single();
// Calculate splits
const COCO_BASE_CENTS = 1000; // $10.00 per order
const COCO_SPLIT_PERCENT = 0.60; // 60% to CoCo
const PLATFORM_SPLIT_PERCENT = 0.40; // 40% to Platform
const cocoShareCents = Math.floor(COCO_BASE_CENTS * COCO_SPLIT_PERCENT); // $6.00
const platformShareCents = Math.floor(COCO_BASE_CENTS * PLATFORM_SPLIT_PERCENT); // $4.00
const chefPaymentCents = order.subtotal_cents; // Food cost to chef
const driverPaymentCents = order.deliveries.delivery_fee_cents;
const transfers = [];
// Transfer to Chef
if (order.chefs.connect_account_id) {
const transfer = await stripe.transfers.create({
amount: chefPaymentCents,
currency: 'usd',
destination: order.chefs.connect_account_id,
transfer_group: `order_${order_id}`,
metadata: { order_id, recipient_type: 'chef' },
});
transfers.push({
order_id,
recipient_type: 'chef',
recipient_id: order.chef_id,
stripe_transfer_id: transfer.id,
amount_cents: chefPaymentCents,
status: 'succeeded',
});
}
// Transfer to CoCo
if (coco?.stripe_account_id) {
const transfer = await stripe.transfers.create({
amount: cocoShareCents,
currency: 'usd',
destination: coco.stripe_account_id,
transfer_group: `order_${order_id}`,
metadata: { order_id, recipient_type: 'coco' },
});
transfers.push({
order_id,
recipient_type: 'coco',
recipient_id: coco.id,
stripe_transfer_id: transfer.id,
amount_cents: cocoShareCents,
status: 'succeeded',
});
}
// Transfer to Driver
if (order.deliveries?.drivers?.connect_account_id) {
const transfer = await stripe.transfers.create({
amount: driverPaymentCents,
currency: 'usd',
destination: order.deliveries.drivers.connect_account_id,
transfer_group: `order_${order_id}`,
metadata: { order_id, recipient_type: 'driver' },
});
transfers.push({
order_id,
recipient_type: 'driver',
recipient_id: order.deliveries.driver_id,
stripe_transfer_id: transfer.id,
amount_cents: driverPaymentCents,
status: 'succeeded',
});
}
// Insert transfer records
await supabaseAdmin.from('payment_transfers').insert(transfers);
return new Response(
JSON.stringify({
success: true,
transfers: transfers.length,
platform_retained_cents: platformShareCents
}),
{ headers: { 'Content-Type': 'application/json' } }
);
});
Step 3: Update Checkout Session to Capture to Platform
Modify: backend/supabase/functions/create_checkout_session/index.ts
// OLD (direct transfer to chef):
const session = await stripe.checkout.sessions.create({
payment_intent_data: {
application_fee_amount: platformFeeCents,
transfer_data: {
destination: chefConnectAccountId, // ❌ Remove this
},
},
// ...
});
// NEW (capture to platform, distribute via webhook):
const session = await stripe.checkout.sessions.create({
payment_intent_data: {
// Platform receives full amount, distributes later
metadata: { order_id: order.id },
},
// ...
});
Step 4: Update Webhook to Trigger Distribution
Modify: backend/supabase/functions/webhook_stripe/index.ts
case 'payment_intent.succeeded': {
const paymentIntent = event.data.object as Stripe.PaymentIntent;
const orderId = paymentIntent.metadata?.order_id;
if (orderId) {
// Update order payment status
await supabaseAdmin
.from('orders')
.update({ payment_status: 'succeeded' })
.eq('id', orderId);
// Trigger payment distribution to all parties
await fetch(`${Deno.env.get('SUPABASE_URL')}/functions/v1/distribute_payment`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${Deno.env.get('SUPABASE_ANON_KEY')}`,
},
body: JSON.stringify({
order_id: orderId,
payment_intent_id: paymentIntent.id,
}),
});
}
break;
}
Similar to chef onboarding, but for drivers:
New Edge Function: backend/supabase/functions/create_driver_connect_account/index.ts
// Nearly identical to create_connect_account for chefs
const account = await stripe.accounts.create({
type: 'express',
country: 'US',
capabilities: {
card_payments: { requested: true },
transfers: { requested: true },
},
metadata: { driver_id, profile_id },
});
await supabaseAdmin
.from('drivers')
.update({ connect_account_id: account.id })
.eq('id', driver_id);
// Return onboarding link...
Frontend: Add onboarding flow to driver profile setup (similar to chef flow)
Since CoCo is a partner organization (not per-user), one Connect account for all orders:
Admin-only operation:
// Run once via admin panel or Supabase SQL Editor
const cocoAccount = await stripe.accounts.create({
type: 'express',
country: 'US',
email: '[email protected]',
capabilities: {
transfers: { requested: true },
},
});
// Store in database
await supabaseAdmin.from('coco_config').insert({
stripe_account_id: cocoAccount.id,
payout_enabled: false, // Set to true after onboarding complete
});
// Complete onboarding via Stripe Dashboard or account link
const accountLink = await stripe.accountLinks.create({
account: cocoAccount.id,
refresh_url: 'https://admin.ridendine.com/coco/onboarding',
return_url: 'https://admin.ridendine.com/coco/dashboard',
type: 'account_onboarding',
});
After implementing multi-party splits:
Test Order Flow:
-- After a test order completes, verify transfers
SELECT
pt.recipient_type,
pt.amount_cents,
pt.status,
pt.stripe_transfer_id,
o.total_cents
FROM payment_transfers pt
JOIN orders o ON pt.order_id = o.id
WHERE o.id = '<test-order-id>';
-- Expected results:
-- chef | <food_cost> | succeeded | tr_...
-- coco | 600 | succeeded | tr_...
-- driver | <delivery_fee_cents> | succeeded | tr_...
-- Platform retains: $4.00 from CoCo split + any additional platform fees
Verify in Stripe Dashboard:
Cause: Platform account doesn't have enough balance (payment hasn't cleared yet)
Fix: Use stripe.transfers.create() with source_transaction parameter to link directly to the payment
const transfer = await stripe.transfers.create({
amount: chefPaymentCents,
currency: 'usd',
destination: chefConnectAccountId,
source_transaction: paymentIntentId, // Links to the specific payment
});
Diagnosis:
-- Check if Connect accounts are set up
SELECT connect_account_id, payout_enabled FROM drivers WHERE id = '<driver-id>';
SELECT stripe_account_id, payout_enabled FROM coco_config;
-- Check transfer records
SELECT * FROM payment_transfers WHERE recipient_id = '<id>' ORDER BY created_at DESC;
Fix:
connect_account_id is NULL → Complete onboardingpayout_enabled is false → Complete Stripe identity verificationFor orders processed before multi-party split implementation:
.claude/skills/stripe-connect-marketplace/SKILL.mdbackend/supabase/functions/development
Integrate Coinbase crypto payments into payment systems. Use when: (1) adding crypto payment support, (2) building onchain features, (3) implementing wallet functionality. Covers Coinbase Commerce (payment processor) vs CDP (developer platform), Server Wallets, Embedded Wallets, and multi-network support.
development
Add Apple Pay and Google Pay to Stripe checkout. Use when: (1) adding mobile wallet payments, (2) improving mobile conversion, (3) implementing one-tap checkout. Stripe Payment Request Button automatically detects device capabilities and shows Apple Pay (Safari/iOS) or Google Pay (Chrome/Android).
development
Master Vercel deployment for RidenDine web and admin Next.js apps. Use when: (1) deploying to production, (2) configuring environment variables, (3) setting up preview deployments, (4) debugging build failures, (5) configuring domains, (6) seeing "No Next.js version detected" error in Vercel builds, (7) setting up monorepo with separate projects on free tier. Key insight: Vercel monorepos require Root Directory configuration via dashboard (not vercel.json), GitHub integration auto-detects monorepo structure, free tier allows multiple projects.
development
Master Supabase Row Level Security (RLS) for RidenDine. Use when: (1) adding new tables, (2) modifying RLS policies, (3) debugging access control issues, (4) role-based data access. Key insight: All tables use RLS with role-based policies from profiles.role column.