Stripe Connect skill for Codex
Marketplace payouts with a traceable money trail. Codex asks who the merchant of record is and who absorbs losses before writing any Connect code, then picks direct, destination or separate-charge flows to match. Sellers stay hidden until account.updated reports charges_enabled, every order traces to its fee and transfer, refunds reverse the seller share deliberately, and the funds-flow design is flagged for legal and finance review.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- stripe-connect Installs to ~/.agents/skills/stripe-connect/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install stripe-connect The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project stripe-connect Installs to .agents/skills/stripe-connect/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/stripe-connect.zipunzip -o stripe-connect.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) stripe-connect PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $stripe-connect, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Designs and builds Stripe Connect marketplaces and platforms - connected account configuration, hosted or embedded onboarding, direct, destination and separate-charges-and-transfers flows, application fees, payouts, refunds with transfer reversal, and Connect webhooks. Use when a platform must pay out sellers, creators or service providers, when choosing a Connect charge type, onboarding connected accounts, handling account.updated requirements, or splitting a payment across several recipients.
Use it when
- A platform collects payments on behalf of sellers, creators or providers
- You must choose between direct, destination and separate-charge flows
- Connected accounts are stuck with outstanding onboarding requirements
- One order needs to be split between several recipients
Use something else when
- Only your own business is paid: use stripe-payments
- You need recurring plans for your own SaaS customers: use stripe-billing
How to check the result
Trace a test order from PaymentIntent to application fee to transfer and seller balance, and confirm a refund with reverse_transfer returns the seller's share.
The mistake it prevents
Calling the API for a seller without the stripeAccount option, so the charge or refund silently happens on the platform account instead.
Done well means: the charge type matches who carries liability and who the customer thinks they
are paying, connected accounts cannot transact until charges_enabled is true, every money
movement is traceable from one order to its charge, fee and transfers, and refunds reverse the
seller’s share as intended.
Platforms that move money for others carry regulatory, tax-reporting and liability obligations. Say plainly that the user’s legal and finance advisers must sign off on the funds-flow design before launch; this skill covers the engineering.
Before writing code
- Read
AGENTS.md; find the existing Stripe client, webhook routes and the model for sellers. - Ask the user (do not assume) these three things, because they fix the design:
- Who is the merchant of record the buyer sees: the platform or the seller?
- Who pays Stripe fees and absorbs losses from disputes and negative balances?
- Does one payment ever go to more than one seller?
- Check the pinned API version and whether the account already uses Connect with legacy
type: 'standard' | 'express' | 'custom'accounts. Keep consistency with existing accounts. - Creating live connected accounts, sending transfers or payouts, and reversing transfers need explicit user confirmation. Work in test mode.
Choose the charge type
| Situation | Charge type | Key parameters |
|---|---|---|
| Seller is the merchant; platform takes a cut (SaaS for businesses) | Direct charge | stripeAccount request option, application_fee_amount |
| Platform is the merchant; one seller per payment (typical marketplace) | Destination charge | transfer_data.destination, application_fee_amount, optionally on_behalf_of |
| One payment split across several sellers, or paid out later | Separate charges and transfers | transfer_group on the charge, then stripe.transfers.create per seller |
Default for a marketplace with one seller per order: destination charge. Use separate charges and transfers only when splitting or delaying is a real requirement; it puts all reconciliation on you.
Connected account configuration
Prefer configuring accounts by controller properties over legacy types:
const account = await stripe.accounts.create({
country: "US",
email: seller.email,
controller: {
fees: { payer: "application" }, // platform pays Stripe fees
losses: { payments: "application" }, // platform covers negative balances
stripe_dashboard: { type: "express" }, // seller gets the Express dashboard
requirement_collection: "stripe", // Stripe collects KYC
},
capabilities: {
card_payments: { requested: true },
transfers: { requested: true },
},
metadata: { sellerId: seller.id },
}, { idempotencyKey: `acct-${seller.id}` });
Store account.id on the seller row immediately. Stripe has also introduced a newer Accounts v2
API; if the project already uses it, follow its shapes rather than mixing versions.
Onboarding
Hosted (fastest):
const link = await stripe.accountLinks.create({
account: seller.stripeAccountId,
type: "account_onboarding",
refresh_url: `${APP_URL}/seller/onboarding/refresh`, // link expired: create a new one
return_url: `${APP_URL}/seller/onboarding/return`, // not proof of completion
});
// redirect to link.url
Embedded: create an Account Session server-side
(stripe.accountSessions.create({ account, components: { account_onboarding: { enabled: true } } }))
and render the onboarding component with Connect.js on the client.
Account Links are single-use and short-lived. Never email them; generate on demand.
Gate on account state, not on the return URL
Subscribe a Connect webhook endpoint (it receives events from connected accounts, with
event.account set) and handle account.updated:
case "account.updated": {
const a = event.data.object as Stripe.Account;
await db.sellers.update({
where: { stripeAccountId: a.id },
data: {
chargesEnabled: a.charges_enabled,
payoutsEnabled: a.payouts_enabled,
requirementsDue: a.requirements?.currently_due ?? [],
disabledReason: a.requirements?.disabled_reason ?? null,
},
});
}
Hide a seller’s listings while charges_enabled is false, and surface currently_due with a
fresh onboarding link. Requirements can reappear later (volume thresholds, document expiry).
Creating charges
// Destination charge via Checkout
await stripe.checkout.sessions.create({
mode: "payment",
line_items: [{ price_data: { currency: "usd", unit_amount: 5000, product_data: { name: listing.title } }, quantity: 1 }],
payment_intent_data: {
application_fee_amount: 750, // platform keeps $7.50
transfer_data: { destination: seller.stripeAccountId },
on_behalf_of: seller.stripeAccountId, // seller's descriptor and country
metadata: { orderId },
},
success_url, cancel_url,
}, { idempotencyKey: `order-${orderId}` });
// Direct charge: same call, made on the connected account
await stripe.paymentIntents.create(
{ amount: 5000, currency: "usd", application_fee_amount: 750, automatic_payment_methods: { enabled: true } },
{ stripeAccount: seller.stripeAccountId, idempotencyKey: `order-${orderId}` },
);
// Separate charges and transfers: after the charge succeeds
await stripe.transfers.create(
{ amount: 4250, currency: "usd", destination: sellerA, transfer_group: `order-${orderId}`, source_transaction: chargeId },
{ idempotencyKey: `xfer-${orderId}-${sellerA}` },
);
source_transaction ties the transfer to the charge so it waits for the funds instead of
failing on an empty platform balance.
Refunds and disputes
| Charge type | Refund call | Seller share |
|---|---|---|
| Destination | refunds.create({ payment_intent, reverse_transfer: true, refund_application_fee: true }) | Pulled back from seller |
| Direct | Refund on the connected account (stripeAccount option) | Seller’s balance pays |
| Separate transfers | Refund the charge, then transfers.createReversal per seller | Each reversal is explicit |
Decide with the user whether the platform keeps its fee on refunds. Disputes on destination charges debit the platform; plan how to recover from the seller.
Payouts
Default to automatic payouts on the connected account’s schedule. Manual payouts
(settings.payouts.schedule.interval: 'manual') make the platform responsible for triggering
them and holding funds; only use when the business requires escrow-like timing and the user has
confirmed it is allowed.
Local testing
stripe listen --forward-to localhost:3000/webhooks/stripe \
--forward-connect-to localhost:3000/webhooks/stripe-connect
Use test-mode onboarding values from Stripe’s docs (test SSN, test bank account) only in test mode. Connect endpoints have their own signing secret.
Gotchas
- Mixing charge types per seller without recording which was used: refunds then go wrong.
- Forgetting
on_behalf_ofwith destination charges across countries: settlement and statement descriptors follow the platform, which may be wrong for the buyer. - Using the platform webhook endpoint for connected-account events: they never arrive there.
- Calling APIs for a connected account without
stripeAccount: you act on the platform. - Fees in the wrong currency:
application_fee_amountis in the charge currency, minor units. - Deleting or rejecting connected accounts is destructive; confirm with the user first.
Verification
- Trace one test order: PaymentIntent, its
application_fee, the transfer to the seller, and the seller balance (stripe balance retrieve --stripe-account acct_...). - A seller with
charges_enabled: falsecannot be purchased from in the UI. - A refund with
reverse_transferreturns the seller’s share and the platform fee as designed. account.updatedevents update the seller row when onboarding completes.
Report template
**Funds flow:** destination charges, platform fee 15%, on_behalf_of seller; platform covers losses
**Accounts:** controller-configured, Express dashboard, Stripe-collected requirements
**Onboarding:** hosted Account Links; gating on charges_enabled via account.updated
**Refund policy:** reverse_transfer + refund_application_fee (confirmed with user)
**Needs legal/finance review:** liability for disputes, payout timing, tax reporting (1099-K etc.) What the zip contains
SKILL.md plus the interface file below. Nothing runs on install.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "Stripe Connect"
short_description: "Marketplace payouts with a traceable money trail"
default_prompt: "$stripe-connect Build seller onboarding for our marketplace with Stripe Connect Express" About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.