← Blog

E-commerce

Stripe Connect audit: 9 checks voordat een agent uitbetaalt

Een custom Stripe Connect account, een capability die niemand opmerkte, een agent die niet wist waar te kijken, en €47.000 die twee weken na payout terugkwam. Dit checken we nu eerst.

Jacob Molkenboer· Oprichter · A Brand New Company· 29 jan 2025· 8 min
Koperen weegschaal, pakje met touw, limegroene kaart, rode lakzegel op ivoorpapier bij een raam.

De mail kwam binnen om 14:07 op een dinsdag. Onderwerp: Negatieve balans op je platformaccount: EUR -47.213,40. De Stripe Connect marketplace had negen dagen eerder uitbetaald aan zijn sellers. Het geld was weg. Twee van de onderliggende charges waren disputed, en omdat de destination accounts hun balans al naar hun bank hadden geveegd, trok Stripe het geld terug van het platform.

De dispute zelf was niet de ramp. Disputes gebeuren. De ramp was dat een automation agent de payout loop had gedraaid zonder op te merken dat een van de connected accounts een transfers capability in pending state had. De agent zag charges_enabled: true, vinkte het af als groen, en ging door. Negen dagen en één chargeback later zat het platform met de gebakken peren.

We hebben de payout flow van die klant op een vrijdagmiddag opnieuw gebouwd. Sindsdien draaien we dezelfde audit voordat een agent die we opleveren geld aanraakt op een Stripe Connect platform. De checklist staat hieronder.

Capability state, niet de enabled flag

De eerste reflex is om elke transfer te laten afhangen van account.charges_enabled en account.payouts_enabled. Allebei booleans. Ze lijken het juiste om te checken. Ze zijn niet genoeg.

Capabilities op een connected account kunnen in vijf states staan: active, pending, inactive, unrequested en disabled. Een custom account kan charges_enabled: true hebben terwijl een specifieke capability van active naar pending glijdt omdat een verificatiedocument is verlopen, of omdat Stripe een nieuwe UBO-verklaring opvraagt. De boolean schakelt pas om als het hele account restricted wordt, en dat kan dagen duren. De transfers die je in die tussentijd doet, dat zijn precies de transfers waar je spijt van krijgt.

Lees de capability expliciet uit voor elke flow die je gaat draaien. De Stripe capabilities reference documenteert de volledige state machine, inclusief welke transities geluidloos verlopen.

// pre-flight before any transfer
const account = await stripe.accounts.retrieve(connectedAccountId)

const checks = {
  charges_enabled: account.charges_enabled === true,
  payouts_enabled: account.payouts_enabled === true,
  transfers_active: account.capabilities?.transfers === 'active',
  card_payments_active: account.capabilities?.card_payments === 'active',
  past_due_empty: (account.requirements?.past_due ?? []).length === 0,
  no_disabled_reason: !account.requirements?.disabled_reason,
}

const safeToMoveMoney = Object.values(checks).every(Boolean)
if (!safeToMoveMoney) {
  await halt(connectedAccountId, checks)
  return
}

Het halt-pad is belangrijker dan de check zelf. De agent stopt, logt welke specifieke check faalde met de waarde die hij zag, en escaleert naar een mens. Hij retried niet. Een retry loop op een ontbrekende capability is hoe je drie dagen later ontdekt dat je dezelfde waarschuwing in een Slack channel hebt gevuurd waar niemand naar kijkt.

Requirements, ook de regels die nog niet due zijn

De account requirements van Stripe vallen in vier buckets: currently_due, eventually_due, past_due en pending_verification. currently_due is de lijst die je volgende week onderuit haalt. eventually_due is de lijst die je over drie maanden onderuit haalt. past_due is de lijst die het account al heeft uitgeschakeld.

Agents checken vrijwel altijd currently_due en slaan de rest over. Dat werkt, tot het deadline-venster aanbreekt en de agent niet weet dat vandaag de dag is waarop een tax ID ingediend moet zijn. Wij behandelen elke niet-lege eventually_due met een current_deadline binnen de komende 14 dagen als een soft halt: de agent vlagt het, een mens ziet het, en er gaat een account onboarding link uit voordat de agent ooit met een harde stop te maken krijgt.

De onboarding link zelf is een zin waard. De account links van Stripe verlopen na een paar minuten. De agent genereert er een opnieuw on demand zodra de seller op de mail klikt, niet vooraf. Dat hebben we geleerd door op een zondagavond 200 sellers verlopen links te sturen.

Webhook-dekking

De meeste Connect-integraties abonneren zich op account.updated en houden het daarbij. Dat dekt veel state-transities, maar niet de transities die duur worden.

De events waarvan we zeker willen zijn dat ze aangesloten zijn voordat een agent live gaat:

  • account.updated voor wijzigingen in capabilities en requirements
  • capability.updated voor state-transities, fijnmaziger dan de parent
  • payout.failed voor het falen aan bankzijde, los van de platform-call
  • payout.paid als bevestiging dat het geld Stripe ook echt heeft verlaten
  • charge.dispute.created als eerste waarschuwing voordat het geld beweegt
  • charge.dispute.funds_withdrawn voor de echte hit op de platformbalans
  • transfer.reversed voor transfers die Stripe heeft teruggedraaid terwijl jij dacht dat ze definitief waren
  • balance.available voor het moment waarop het geld echt beschikbaar wordt op het platform

Elke event heeft een idempotente handler nodig en een plek in je eigen database. De agent leest uit je database voordat hij een beslissing neemt, niet uit de live Stripe API bij elke loop. De API is de bron van waarheid. Je database is de bron van consistentie tussen agentbeslissingen.

Reserve-rekenwerk dat de agent niet kent

Als het platform 100 procent van de beschikbare balans uitbetaalt aan sellers en de volgende dag landt er een dispute, dan eet het platform het op. Dit geldt ook als het account van de seller intussen leeg is. De account balances guide is daar expliciet over: een negatieve connected account balans wordt een platformverplichting op het moment dat het geld niet meer bij de seller te halen is.

Kies een reserve. Wij houden standaard 7 tot 10 procent van het bruto volume vast gedurende de eerste 90 dagen van elke nieuwe seller, en verlagen het percentage daarna op basis van dispute-historie. De agent beslist niet wat er uitbetaald wordt. Hij leest een target payout uit een functie die het bruto, het reservepercentage, de rolling 60-daagse dispute rate en eventuele open disputes meeneemt, en die één getal teruggeeft.

Waarschuwing

Een automation agent zonder reservebeleid betaalt vroeg of laat geld uit dat het platform aan Stripe verschuldigd is. De agent is niet het vangnet. De reserveberekening is dat.

Idempotency op elke transfer

Elke transfers.create call heeft een Idempotency-Key nodig. Agents die een transient error raken en zonder key retryen, hebben dezelfde transfer twee keer aangemaakt. Wij gebruiken een deterministische sleutelvorm, zodat hetzelfde payout-slot nooit dubbel kan vuren.

const idempotencyKey = `payout:${connectedAccountId}:${payoutDateISO}:v1`

await stripe.transfers.create(
  {
    amount: amountInCents,
    currency: 'eur',
    destination: connectedAccountId,
    transfer_group: `payout-batch:${payoutDateISO}`,
  },
  { idempotencyKey }
)

Zelfde key, zelfde dag, zelfde destination, zelfde resultaat. Het v1-suffix staat er voor de dag waarop we het payout-schema veranderen en een schone breuk nodig hebben. Zonder dat suffix heb je geen manier om een payout legitiem opnieuw uit te voeren.

Het charge-model bepaalt wie het verlies eet

Eén detail dat het €47k-incident niet direct veroorzaakte, maar wel verergerde: het platform gebruikte destination charges met on_behalf_of. Het connected account is dan merchant of record. Disputes gaan eerst tegen het connected account, maar het geld wordt van het platform afgetrokken als het connected account het niet kan dekken. Als hetzelfde platform op separate charges and transfers had gezeten, was het dispute-geld direct uit de platformbalans zelf gekomen. Dat klinkt erger, maar is makkelijker te overzien, omdat er één plek is om naar te kijken.

Geen van beide modellen is fout. Kies het model dat aansluit bij hoe je boekhouding en je dispute-response team echt werken, en zorg dat de agent weet onder welk model hij draait. De pre-flight check ziet er in elk geval anders uit.

Dispute-replay in test mode

Voordat een agent live gaat op een Connect-platform, draaien we een synthetische chargeback in test mode tegen een connected account dat al uitbetaald heeft gekregen. Stripe laat je dit triggeren met de testkaart 4000 0000 0000 0259, die een paar minuten na de charge een dispute genereert.

Het doel is niet om te testen dat de dispute gebeurt. Het doel is om de platformbalans negatief te zien gaan en te bevestigen dat de agent het juiste doet: nieuwe payouts stoppen, de negatieve balans zichtbaar maken, en de alert naar een mens routeren. Als de agent doorgaat met uitbetalen aan andere sellers terwijl het platformsaldo onder water staat, dan is de audit gefaald en gaat de agent terug naar staging.

Waar dit stilletjes faalt

De rode draad door alles hierboven is observability. Een Stripe capability die van active naar pending glijdt, crasht niets. Een agent met een zelfverzekerde prompt en geen instrumentatie zal je niet vertellen dat het misging. De dure faalmodus van een agentic systeem is niet wanneer het een error gooit. Het is wanneer het overtuigend het verkeerde doet, in een schone run, met een groene log line onderaan.

Voor agents die geld verplaatsen geldt hetzelfde als voor elk productiesysteem. Elke beslissing wordt gelogd met de inputs die hij zag. Elk halt-pad is luid. De mens aan de andere kant krijgt een dagelijkse digest van dit is wat ik bijna deed en wat ik bewust niet heb gedaan. Als je niet kunt aanwijzen op welke logregel de agent de ontbrekende capability zag en stopte, dan is de agent niet veilig om te draaien.

De versie van vijf minuten

De volledige audit is zo'n 40 punten lang. De korte versie, die je in een middag kunt draaien voordat je een agent met echt geld laat werken:

  1. Trek alle connected accounts op en bevestig capabilities.transfers === 'active' en capabilities.card_payments === 'active' voor de flows die je daadwerkelijk gebruikt.
  2. Vlag elk account met een current_deadline binnen de komende 14 dagen.
  3. Bevestig dat je webhook-abonnementen de acht events hierboven dekken en dat elke handler idempotent is.
  4. Schrijf op wat het reservepercentage is en waar het in code staat. Als niemand die vraag in één zin kan beantwoorden, heb je geen reservebeleid.
  5. Speel één synthetische dispute af in test mode van begin tot eind en kijk wat de agent doet zodra het platformsaldo negatief wordt.

Toen we de marketplace payout agent bouwden voor de klant achter het €47k-verhaal, liepen we ertegenaan dat active capabilities midweeks pending kunnen worden zonder dat er een enkele error vuurt. We hebben het opgelost door de capability state bij elke payout pre-flight uit te lezen en elke niet-active waarde te behandelen als een harde halt met escalatie naar een mens. Dezelfde checklist zit achter elke AI-agent die we opleveren op een payments platform.

De versie van vandaag in vijf minuten: open je Stripe dashboard, ga naar Connect, sorteer accounts op current deadline oplopend, en kijk naar de top vijf. Daar zit je volgende clawback verstopt.

Kern

charges_enabled is een boolean. De capability erachter is een state machine. Lees die state machine voordat een agent geld verplaatst.

FAQ

Waarom is charges_enabled niet genoeg als gate voor een payout?

charges_enabled is een grove boolean. Individuele capabilities zoals transfers kunnen van active naar pending bewegen terwijl die boolean true blijft. Lees de capability state direct uit voor elke transfer.

Wat is het verschil tussen currently_due en eventually_due requirements?

currently_due zet het account uit als het niet voor de huidige deadline is aangeleverd. eventually_due is wat Stripe daarna gaat opvragen. Agents die alleen currently_due checken, lopen tegen de muur zodra het venster omklapt.

Met welk reservepercentage start een nieuwe marketplace?

7 tot 10 procent van het bruto volume gedurende de eerste 90 dagen van elke nieuwe seller is een redelijke default. Pas het aan op basis van dispute rate, gemiddelde tickethoogte en refundfrequentie. Er is geen universeel getal.

Kan een automation agent veilig op Stripe-disputes reageren?

Ja voor het verzamelen en indienen van bewijs. Nee voor de beslissing of je een dispute aanvecht. Een dispute is een beschuldiging vanuit de bank van een klant, en het antwoord beïnvloedt je status als merchant.

Welke testkaart triggert een dispute in Stripe test mode?

4000 0000 0000 0259 charget succesvol en genereert een paar minuten later een dispute. Gebruik die kaart om het negatieve-balans scenario af te spelen voordat een agent live gaat.

ai agentsautomationintegrationse-commerceoperationsarchitecture

Iets bouwen?

Start een project