🔧 Programmierung5 Things AI Cannot Do at PostgreSQL(16.09.2026 um 23:55 Uhr)
🔧 ProgrammierungI Said Install ffmpeg. I Did Not Say Rewrite My Machine.(17.09.2026 um 00:13 Uhr)
🔧 ProgrammierungGenerative AI automates quantum optimization circuit design(17.09.2026 um 00:33 Uhr)
🔧 ProgrammierungBuilding the new GitHub Copilot Inline Suggestions Model: Part One(16.09.2026 um 02:00 Uhr)
🔧 Programmierung5 Things AI Cannot Do at PostgreSQL(16.09.2026 um 23:55 Uhr)
🔧 ProgrammierungI Said Install ffmpeg. I Did Not Say Rewrite My Machine.(17.09.2026 um 00:13 Uhr)
🔧 ProgrammierungGenerative AI automates quantum optimization circuit design(17.09.2026 um 00:33 Uhr)
🔧 ProgrammierungBuilding the new GitHub Copilot Inline Suggestions Model: Part One(16.09.2026 um 02:00 Uhr)
🔧 Programmierung 🕛 vor 5 Monaten 13 Min Lesezeit
0

Stripe Closed My Connect Account. Here's What Actually Fixed It in 24 Hours.

↗ Quelle (dev.to)
🗣️ Stimme:
📑 Inhaltsübersicht

I'm building .




I went with Express for one reason: Stripe handles identity verification and onboarding for you.



When a creator opens a Stripe account, they need to verify their identity and register a bank account. Building those screens yourself is a massive time sink. With Express, Stripe provides the entire onboarding flow — a huge win for solo developers.




CODE
const account = await stripe.accounts.create({
type: 'express',
country: 'JP',
capabilities: {
card_payments: { requested: true },
transfers: { requested: true },
},
business_type: 'individual',
});












Legal requirements before setting up Stripe Connect



This gets overlooked surprisingly often. In practice, Stripe's review team expected these pages to be live on my site before approval:




  1. Privacy Policy

  2. Terms of Service


  3. E-commerce disclosure page — In Japan, this is required under the Act on Specified Commercial Transactions (特定商取引法). It's prescriptive: you must publish your seller name, address, return policy, and pricing. Other countries have analogous requirements.



This isn't Connect-specific. It's a prerequisite for accepting any payments. Get these pages live before you start the Connect application.




Heads up: Stripe's review team flagged the title of my disclosure page. The exact wording needed to match the legally prescribed format. They also required my seller name and responsible person to match my Stripe account registration exactly. Small details, but they'll hold up your review.










Why Stripe flagged my platform as aggregation



When the "Your account has been closed" email arrived, I froze. I had just finished wiring up the Supabase RLS policies. The platform was ready. And then Stripe shut the account down.



I'd previously integrated Stripe Connect for another project (Sapolova, a creator support platform), and that review went smoothly. The business model was simpler, and the review was straightforward.



Lovai was different. Stripe closed my account.






What Stripe told me



The email said Lovai's business fell under "aggregation" — one of their : Stripe processes the charge on the platform account, routes funds to the creator's connected account, returns the application fee to the platform, and debits Stripe processing fees from the platform balance.









Stripe Connect Express: onboarding implementation



This is the flow for creators to set up their Stripe Connect account.




CODE
export async function createConnectAccount() {
const userId = await getAuthUserId();

// Check for existing account (prevent duplicates)
const { data: existingAccount } = await serviceClient
.from('creator_stripe_accounts')
.select('stripe_account_id, details_submitted')
.eq('user_id', userId)
.maybeSingle();

let accountId: string;

if (existingAccount) {
accountId = existingAccount.stripe_account_id;
} else {
const account = await stripe.accounts.create({
type: 'express',
country: 'JP',
capabilities: {
card_payments: { requested: true },
transfers: { requested: true },
},
business_type: 'individual',
metadata: { lovai_user_id: userId },
});
accountId = account.id;

await serviceClient.from('creator_stripe_accounts').insert({
user_id: userId,
stripe_account_id: accountId,
charges_enabled: false,
payouts_enabled: false,
details_submitted: false,
});
}

const accountLink = await stripe.accountLinks.create({
account: accountId,
refresh_url: `${baseUrl}/settings/payments?refresh=true`,
return_url: `${baseUrl}/settings/payments?success=true`,
type: 'account_onboarding',
});

return { data: { url: accountLink.url } };
}






Storing the Lovai user ID in metadata lets you identify which user owns which Stripe account. The charges_enabled, payouts_enabled, and details_submitted statuses are synced from the Stripe API to your local DB.









Checkout with destination charges (transfer_data)



This handles what happens when a buyer purchases paid content. Lovai uses Stripe's hosted Checkout — redirecting to their payment page.




CODE
export async function createPurchaseCheckoutSession(postId: string) {
const userId = await getAuthUserId();

const post = await getPost(postId);
if (!post) return { error: { code: 'NOT_FOUND' } };
if (!post.has_premium || !post.price_yen) return { error: { code: 'NOT_PREMIUM' } };
if (post.author_id === userId) return { error: { code: 'OWN_POST' } };

const existingPurchase = await getExistingPurchase(postId, userId);
if (existingPurchase?.status === 'completed') return { error: { code: 'ALREADY_PURCHASED' } };

const creatorAccount = await getCreatorStripeAccount(post.author_id);
if (!creatorAccount?.charges_enabled) return { error: { code: 'CREATOR_NOT_READY' } };

const priceAmount = post.price_yen;
const applicationFee = calculatePlatformFee(priceAmount);

const session = await stripe.checkout.sessions.create({
mode: 'payment',
payment_method_types: ['card'],
line_items: [{
price_data: {
currency: 'jpy',
product_data: {
name: post.title,
description: 'Premium content purchase',
},
unit_amount: priceAmount,
},
quantity: 1,
}],
payment_intent_data: {
application_fee_amount: applicationFee,
transfer_data: {
destination: creatorAccount.stripe_account_id,
},
},
success_url: `${baseUrl}/post/${postId}?purchase=success`,
cancel_url: `${baseUrl}/post/${postId}?purchase=cancelled`,
metadata: {
post_id: postId,
buyer_id: userId,
author_id: post.author_id,
price_yen: String(priceAmount),
},
});

await serviceClient.from('post_purchases').insert({
post_id: postId,
buyer_id: userId,
price_yen: priceAmount,
stripe_checkout_session_id: session.id,
status: 'pending',
});

return { data: { url: session.url } };
}






Three parameters that matter:




























Parameter What it does What breaks without it
transfer_data.destination Routes funds to the creator's Stripe account Payment succeeds but creator never gets paid
application_fee_amount Your platform fee — Stripe deducts this and sends it to you You earn nothing from the transaction
metadata Identifies which post was purchased and by whom Webhook can't update the right purchase record


The CREATOR_NOT_READY check matters. Specifying transfer_data when the creator's Stripe account isn't active causes an API error. Lovai caches charges_enabled locally but re-checks via the Stripe API before creating each checkout session. Cache alone would miss cases where Stripe deactivated an account.









Webhook signature verification and idempotency



The webhook receives payment completion events from Stripe and updates purchase records.






Signature verification (non-negotiable)






CODE
export async function POST(req: NextRequest) {
const body = await req.text();
const signature = req.headers.get('stripe-signature');

let event: Stripe.Event;
try {
event = stripe.webhooks.constructEvent(body, signature!, webhookSecret);
} catch (err) {
return NextResponse.json({ error: 'Invalid signature' }, { status: 400 });
}
// ...
}







Never skip signature verification. Without it, anyone can send fake webhook requests and manipulate purchase records. Similarly, SUPABASE_SERVICE_ROLE_KEY is server-only — never include it in client-side code.







Three-layer idempotency



Stripe webhooks can deliver the same event multiple times. This implementation prevents duplicate processing with three layers: event deduplication, purchase status check, and earnings duplicate prevention.



Layer 1: Event deduplication via unique constraint




CODE
create table public.stripe_webhook_events (
id bigint generated always as identity primary key,
event_id text not null unique,
event_type text not null,
processed_at timestamptz default now() not null
);






If two webhooks for the same event arrive simultaneously, only the first insert succeeds. The second hits the unique constraint violation — no race condition.



Layer 2: Purchase status check




CODE
if (existingPurchase?.status === 'completed') {
return NextResponse.json({ received: true });
}






Layer 3: Earnings duplicate prevention




CODE
async function createCreatorEarning(params) {
const { purchaseId, creatorId, postId, grossAmount } = params;

const { data: existing } = await supabase
.from('creator_earnings')
.select('id')
.eq('purchase_id', purchaseId)
.maybeSingle();

if (existing) return;

const platformFee = calculatePlatformFee(grossAmount);
const stripeFee = calculateStripeFee(grossAmount);
const netAmount = grossAmount - platformFee - stripeFee;

await supabase.from('creator_earnings').insert({
creator_id: creatorId,
purchase_id: purchaseId,
post_id: postId,
gross_amount: grossAmount,
platform_fee: platformFee,
stripe_fee: stripeFee,
net_amount: netAmount,
status: 'pending',
});
}






Storing gross_amount, platform_fee, stripe_fee, and net_amount separately means you can trace exactly which formula produced each record when you adjust fees later.




For simplicity, this shows sequential operations. In production, if the DB update fails after the event is logged, you get an inconsistent state. Ideally, wrap event logging, purchase update, and earnings creation in a single transaction. If that's not feasible, use a recoverable job queue.







Earnings calculation



For a 500 JPY (~$3.50 USD) purchase:

















Item Amount
Sale price (gross) 500 JPY
Stripe processing fee 3.6% for domestic cards in Japan ( for current limits. Fees and minimums differ by payment method (cards, convenience store payments, bank transfers). Always verify actual amounts in your Stripe Dashboard.










Purchase records secured with Supabase RLS






Table definition






CODE
create type public.purchase_status as enum (
'pending',
'completed',
'refunded'
);

create table public.post_purchases (
id uuid primary key default gen_random_uuid(),
post_id uuid not null references public.posts(id) on delete restrict,
buyer_id uuid not null references public.profiles(id) on delete cascade,
price_yen integer not null,
stripe_payment_intent_id text,
stripe_checkout_session_id text,
status public.purchase_status default 'pending' not null,
created_at timestamptz default now() not null,
completed_at timestamptz,
unique (post_id, buyer_id)
);






Design decisions:





  • unique (post_id, buyer_id) — Prevents double-purchasing.


  • on delete restrict — Prevents creators from deleting purchased posts. Buyers paid for that content; it shouldn't vanish. Creators can change a post's status instead.






RLS: users can read, only the server can write






CODE
create policy "post_purchases_select_own"
on public.post_purchases for select
using (buyer_id = (select auth.uid()));

create policy "post_purchases_select_author"
on public.post_purchases for select
using (
exists (
select 1 from public.posts p
where p.id = post_purchases.post_id
and p.author_id = (select auth.uid())
)
);






INSERT, UPDATE, and DELETE are not permitted for any user. All mutations go through the webhook via the Service Role client.



If UPDATE were open, a user could flip their purchase from pending to completed without paying. I covered this in detail in my article on



Try Lovai: AI recipes and dev workflows, shared block by block

Vollständiger Original-Artikel
Den kompletten Beitrag mit allen Details direkt auf dev.to lesen.
↗ Original-Artikel auf dev.to lesen
Wie bewertest du diesen Beitrag?
1 Klick Feedback
Teilen mit Netzwerk & Team:

Community-Analysen & Experten-Meinungen 0

Verfasse deine eigene Analyse, teile Workarounds oder diskutiere diesen Vorfall im Blog.
Noch keine Community-Analyse verfasst. Markiere einen Textabschnitt oder klicke oben auf Eigene Analyse verfassen“!
Community Pulse: Relevanz-Einschätzung
1 Klick Experten-Votum
🔴 Akute Relevanz 0%
🟡 In Evaluierung 0%
🟢 Keine Auswirkung 0%
Spannende Innovation 0%
Verwandte Story-Cluster & Quellen (Vektor-KI)
Port 8095 Engine
1 Quelle
Smart Country Convention Werkzeuge für digitale Souveränität - Kommune 21
1 Quelle
AI agents can modify themselves without humans telling them to do so
1 Quelle
Spotminder’s trackable passport holder keeps tabs on your travel docs, so you can relax
Ähnliche Beiträge
🔍 Verwandte News

Auch interessante Nachrichten Stripe Closed My Connect Account. Here's What Actually Fixed It in 24 Hours.

Thematisch verwandte Begriffe: Stripe, Closed, Connect, Account · 6 Treffer

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Zum Aktualisieren ziehen
ZERO-DAY Kritische Sicherheitsmeldung
Advisory →
TTS Reader • tsecurity.de Voice
tsecurity.de Icon
tsecurity.de App
Offline-Lesen, Eilmeldungen & 0ms Ladezeit

Installiere tsecurity.de direkt auf deinen Home-Bildschirm für das ultimative Vollbild-Magazinerlebnis ohne Browser-Leisten.

Nächster Beitrag
Themen-Radar & Intelligence Matrix
Echtzeit-Taxonomie nach Angriffsvektoren & Plattformen
Community Radar & Live Chat
Sentinel Bot online • Live-Stream
Dein Cluster: Security Explorer
Match:
lädt…
Verbindung zum Community-Stream wird aufgebaut...
Aktivitäten deiner Analysten
lädt…
Neues Thema oder Eilmeldung einreichen

Reiche interessante Links, Zero-Days oder Debatten ein. Die Community entscheidet per Upvote über die Veröffentlichung.

Heiß diskutierte Einreichungen
🔖 Gespeicherte Artikel
📂 Keine gespeicherten Artikel vorhanden.
News ⏱️ 3 Min vor 10 Min
Artikeldaten werden geladen...

↗ Original-Quelle