Iblai Vibe Monetization Onboard
BSecurityBuild the Stripe Connect Express onboarding surface for Platform sellers — the status card with Configured / Incomplete / Not Configured badge, the Configure Stripe / Complete Setup / Stripe Dashboard action button, the onboard-refresh round-trip when an onboarding link expires, and the is_ready_for_payments gate every later paywall depends on. Use when the user mentions Stripe Connect, Connect Express, onboarding, status card, is_ready_for_payments, dashboard link, disconnect Stripe, or "the...
- 15 stars
- 0 votes
- 0 copies
- 3 views
- Added September 2, 2026
Works with
Security analysis
75/100- Exfiltrates credentials via HTTP — exact pattern from Snyk ToxicSkills study
Pro scans all 2 files and shows the line behind each finding
npx -y skills add iblai/vibe --skill iblai-vibe-monetization-onboard --agent claude-codeAre you the author of Iblai Vibe Monetization Onboard?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/iblai-iblai-vibe-monetization-onboard)---
name: iblai-vibe-monetization-onboard
description: Build the Stripe Connect Express onboarding surface for Platform sellers — the status card with Configured / Incomplete / Not Configured badge, the Configure Stripe / Complete Setup / Stripe Dashboard action button, the onboard-refresh round-trip when an onboarding link expires, and the is_ready_for_payments gate every later paywall depends on. Use when the user mentions Stripe Connect, Connect Express, onboarding, status card, is_ready_for_payments, dashboard link, disconnect Stripe, or "the Platform can't accept payments yet". See /iblai-vibe-monetization for the family overview, /iblai-vibe-monetization-configure for what comes next once Connect is ready, /iblai-vibe-monetization-checkout for the buyer flow, /iblai-vibe-account for the page that hosts the MonetizationTab, /iblai-vibe-auth for token wiring, /iblai-vibe-rbac for IsPlatformAdmin.
globs:
alwaysApply: false
metadata:
kind: ui
---
# /iblai-vibe-monetization-onboard
Build the Stripe Connect Express onboarding surface — the first thing a
Platform owner sees on the Monetization tab. Until this surface flips
`is_ready_for_payments` to true, every paywall, price tier, and
checkout you wire up later will refuse to charge.
> **Common setup (brand, conventions, env files, verification):** see [docs/skill-setup.md](https://raw.githubusercontent.com/iblai/vibe/refs/heads/main/docs/skill-setup.md).
> **Verify the API before you call it.** Fetch the live OpenAPI schema at `{dm_url}/api/docs/schema/` (browsable at `{dm_url}/api/docs/`; `{dm_url}` = `https://api.$DOMAIN/dm`, `DOMAIN` from `iblai.env`, default `iblai.app`) and confirm the URL path, method, request body, and response shape for every endpoint you reach for. The schema is the source of truth; the URLs in this skill exist for orientation and may drift between releases. See [/iblai-vibe-monetization → references/schema-validation.md](../../billing/iblai-vibe-monetization/references/schema-validation.md) for the exact fetch routine.
## Prerequisites
- Auth must be set up first (`/iblai-vibe-auth`) — reuse the same token wiring
- `.mcp.json` configured with `@iblai/mcp` (and skills installed via
`npx skills add iblai/vibe --all`)
- `iblai.env` populated with `PLATFORM`, `DOMAIN`, `TOKEN`. If missing:
`curl -o iblai.env https://raw.githubusercontent.com/iblai/vibe/refs/heads/main/iblai.env`
- The Account page already exists (`/iblai-vibe-account`) — the
Stripe Connect status card lives inside the `Monetization` profile
tab on that page, not on a standalone route
- The caller holds the `IsPlatformAdmin` role on the Platform. Connect
endpoints are owner-only — non-owners get `403` and the status query
comes back with `is_owner: false`, which causes the SDK to hide the
card entirely
- The Platform has `enable_monetization === true` on its
`currentTenant` record. This flag is what surfaces the
`Monetization` tab inside `Account` in the first place; if it is
unset, the entire onboarding surface is invisible regardless of
Connect state. See `/iblai-vibe-monetization` for the flag lifecycle.
## What you'll build
A single Stripe Connect status card (the SDK component
`StripeConnect` from `@iblai/iblai-js`) with three states:
| State | Badge | Description | Action button |
|---|---|---|---|
| Ready | `Configured` (blue) | "Stripe is connected and ready to accept payments." Optionally appends "Charges enabled." / "Payouts enabled." | `Stripe Dashboard` — opens the Stripe-hosted Express Dashboard in a new tab |
| Started but incomplete | `Incomplete` (yellow) | "Stripe account created but onboarding is incomplete. Complete setup to start accepting payments." | `Complete Setup` — re-issues the onboarding link and redirects |
| Not started | `Not Configured` (grey) | "Connect a Stripe account to enable monetization features." | `Configure Stripe` — kicks off onboarding |
The badge and button text come straight from the SDK — do not
re-skin them. The badge maps to the `is_ready_for_payments` /
`has_account` booleans on the status response, not to
`onboarding_complete` directly.
This card is **auto-rendered** inside `MonetizationTab` whenever the
status response carries `is_owner: true`. You do not mount it by
hand. The reason this skill exists is so that (a) you understand the
contract before you put the tab on a page, and (b) you can build a
custom Connect surface (for example, a setup wizard, an admin tool,
or a standalone `/billing/setup` route) that talks to the same five
endpoints when the default card does not fit.
## Step 1: Validate the API schema
Before you write a single Connect call, walk through
[`references/connect-api.md`](./references/connect-api.md) — it
documents the five endpoints, request bodies, response fields,
permission classes, and the SDK hook map.
Then cross-check each path against the live schema as the schema-first
directive prescribes. The five Connect paths today (`commerce` tag) —
all hosted under the **DM base** (`{dm_url}`, e.g.
`https://api.iblai.app/dm`):
- `GET {dm_url}/api/service/platforms/{platform_key}/stripe/connect/status/`
- `POST {dm_url}/api/service/platforms/{platform_key}/stripe/connect/onboard/`
- `POST {dm_url}/api/service/platforms/{platform_key}/stripe/connect/onboard/refresh/`
- `GET {dm_url}/api/service/platforms/{platform_key}/stripe/connect/dashboard/`
- `DELETE {dm_url}/api/service/platforms/{platform_key}/stripe/connect/`
All five require `IsPlatformAdmin` and run platform-owner validation
in the service layer on top of that. All require `Authorization: Token
<DM token>` — the **DM token**, not the AXD token; the two are different
tokens issued by different services. Using the AXD token returns `401`.
## Step 2: Mount the status query and gate on `is_ready_for_payments`
Mount the status query and read `is_ready_for_payments`:
```tsx
'use client';
import { useGetStripeConnectStatusQuery } from '@iblai/iblai-js/data-layer';
export function ConnectGate({ platformKey }: { platformKey: string }) {
const { data, isLoading, isError } = useGetStripeConnectStatusQuery(
{ platform_key: platformKey },
{ refetchOnMountOrArgChange: true },
);
if (isLoading) return <p>Checking Stripe…</p>;
if (isError) return <p>Stripe status unavailable.</p>;
if (!data?.is_owner) return null; // hide card for non-owners
if (!data.is_ready_for_payments) {
return <p>Finish Stripe onboarding before publishing paywalls.</p>;
}
return <p>Ready to accept payments.</p>;
}
```
`is_ready_for_payments` is the only field you should branch on for the
"can this Platform accept payments today?" decision. It composes
`!is_disconnected && onboarding_complete && charges_enabled` on the
backend — `has_account: true` flips long before payments work and
will mislead you if you check it directly.
`{ refetchOnMountOrArgChange: true }` matters here: after the Stripe
redirect lands the user back on your page, the cached status from
before onboarding is stale. The default SDK card sets this option for
the same reason.
## Step 3: Trigger onboarding
Use the onboarding mutation. The hook lives at the same export path:
```tsx
'use client';
import { useStartStripeConnectOnboardingMutation } from '@iblai/iblai-js/data-layer';
function ConfigureButton({ platformKey }: { platformKey: string }) {
const [startOnboarding, { isLoading }] =
useStartStripeConnectOnboardingMutation();
const handleClick = async () => {
const returnUrl = new URL(window.location.href);
returnUrl.searchParams.set('profileTab', 'monetization');
const url = returnUrl.toString();
const result = await startOnboarding({
platform_key: platformKey,
return_url: url,
refresh_url: url,
business_type: 'company', // SDK default
}).unwrap();
window.location.href = result.onboarding_url;
};
return (
<button onClick={handleClick} disabled={isLoading}>
Configure Stripe
</button>
);
}
```
A few things to know:
- The SDK's bundled `StripeConnect` component **hardcodes
`business_type: 'company'`** today. If you need the `individual`
variant (sole traders, freelancers selling their own agent), do
**not** try to override the SDK card — build a custom onboarding-start
screen against the same mutation and pass `business_type:
'individual'`. The schema enum accepts both values and defaults to
`individual` server-side when the field is omitted.
- `return_url` and `refresh_url` are both **required** by the request
serializer. Using the same URL for both (as the SDK does) is fine.
- Redirect with `window.location.href`. Do not open the onboarding
URL in an iframe — Stripe Connect blocks framing.
- The mutation invalidates the `stripeConnectStatus` cache tag, so any
status query already mounted in your tree will refetch automatically
once the user returns.
## Step 4: Handle return + refresh
After the owner completes (or abandons) onboarding, Stripe redirects
the browser back to the URL you supplied:
- **`return_url`** — fires on completion. Your page should re-mount,
the status query should refetch, `is_ready_for_payments` should now
be true.
- **`refresh_url`** — fires when the onboarding link expires (Stripe
Connect links are short-lived) or when the owner exits the flow.
Your page should detect this and re-issue an onboarding URL by
calling the refresh endpoint:
```ts
// All Connect endpoints are mounted on the DM service, not the AXD edge.
// Compose the base from your env or from SERVICES.DM if the SDK is loaded.
const dmBase = `${apiBase}/dm`; // e.g. https://api.iblai.app/dm
const res = await fetch(
`${dmBase}/api/service/platforms/${platformKey}/stripe/connect/onboard/refresh/`,
{
method: 'POST',
headers: {
// DM token — not the AXD token. The SDK injects this automatically
// when you go through RTK Query; the direct fetch needs it spelled.
Authorization: `Token ${dmToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
return_url: window.location.href,
refresh_url: window.location.href,
business_type: 'company',
}),
},
);
const { onboarding_url } = await res.json();
window.location.href = onboarding_url;
```
In Next.js, your App Router page **must not strip the query params
Stripe appends** (`?profileTab=monetization` is what tells your tab
which subview to show). If you read `searchParams` server-side, pass
them through; never use `router.replace('/account')` without
preserving the query string on the return landing.
## Step 5: Show the dashboard link
Once `is_ready_for_payments` is true, surface a button that drops the
owner into the Stripe-hosted Express Dashboard:
```tsx
import { useLazyGetStripeConnectDashboardQuery } from '@iblai/iblai-js/data-layer';
function DashboardButton({ platformKey }: { platformKey: string }) {
const [getDashboard, { isFetching }] = useLazyGetStripeConnectDashboardQuery();
const open = async () => {
const { data } = await getDashboard({ platform_key: platformKey });
if (data?.dashboard_url) window.open(data.dashboard_url, '_blank');
};
return <button onClick={open} disabled={isFetching}>Stripe Dashboard</button>;
}
```
Use the lazy variant — the dashboard URL is one-shot and tied to the
requesting session, so you fetch on click rather than on mount.
`useGetStripeConnectDashboardQuery` works too if you want
auto-fetching, but it will issue a request every time the component
mounts whether the user clicks or not.
## Step 6: Disconnect (optional, admin-only)
`DELETE {dm_url}/api/service/platforms/{platform_key}/stripe/connect/` is
defined in the schema and implemented at `StripeConnectDisconnectView`
(line 155 of `views/stripe_connect.py`), but the SDK does not currently
expose a hook for it. If you need to surface a disconnect option in a
custom admin tool, call it directly:
```ts
// Compose the DM base — Stripe Connect lives on DM, not the AXD edge.
const dmBase = `${apiBase}/dm`;
await fetch(
`${dmBase}/api/service/platforms/${platformKey}/stripe/connect/`,
{ method: 'DELETE', headers: { Authorization: `Token ${dmToken}` } },
);
```
Response shape:
- **200** — disconnect succeeded; the `StripeConnectAccount` row was
soft-deleted.
- **404** — the Platform has no Stripe Connect account row to delete
(nothing was ever onboarded, or it was already disconnected).
The endpoint soft-deletes the `StripeConnectAccount` row — paywalls
keep their configuration but cannot charge until the Platform
re-onboards. Confirm twice in the UI before calling it; this is
production-grade revenue infrastructure.
## Verify
- [ ] `Monetization` tab appears inside the `Account` page only when
`currentTenant?.enable_monetization === true`
- [ ] Status card renders **only** when the status response has
`is_owner: true`
- [ ] Before onboarding: badge reads `Not Configured`, button reads
`Configure Stripe`
- [ ] Clicking `Configure Stripe` redirects to a URL on
`connect.stripe.com` (not a same-origin error page)
- [ ] After returning from Stripe, the status query refetches and
`is_ready_for_payments` is `true`
- [ ] Badge then reads `Configured`, button reads `Stripe Dashboard`
- [ ] Clicking `Stripe Dashboard` opens a Stripe-hosted URL in a new tab
- [ ] If you exited mid-flow, hitting the `refresh_url` re-issues an
onboarding link without prompting for any data twice
- [ ] Non-owner Platform admins (`is_owner: false`) see the tab but
not the Connect card
## Common mistakes
- **Gating off `has_account: true` instead of `is_ready_for_payments`.**
`has_account` flips the moment the Connect account row is created,
which is before KYC, before charges are enabled, and before Stripe
will accept a payment intent. Reading it as "Stripe works now" is
the most common reason paywalls publish but checkout fails with a
Stripe error.
- **Skipping the `is_ready_for_payments` check entirely** and assuming
any user on the Monetization tab can configure paywalls. The
Monetization tab's wizard is gated against this flag — surfacing
pricing UI on top of an unready Connect account just produces a
broken checkout downstream.
- **Stripping query params on the return URL.** Next.js router methods
like `router.replace('/account')` drop `?profileTab=monetization`,
which leaves the owner on the wrong sub-tab when they return. Either
read `searchParams` and preserve them, or use
`window.location.href` for the redirect so Stripe round-trips you to
the exact URL you supplied.
- **Forgetting to pass `authURL` to the `<Account>` page.** The
MonetizationTab needs the auth-redirect URL to chain the user
through if they hit a re-auth wall during onboarding. See
`/iblai-vibe-account` for the prop contract.
- **Trying to override `business_type: 'company'` by patching the SDK
card.** The string is hardcoded inside the component; reach for a
custom screen against `useStartStripeConnectOnboardingMutation`
instead, and pass `business_type: 'individual'` there.
- **Treating the dashboard URL as cacheable.** It is one-shot. Re-call
the endpoint each time the owner clicks the button.
## MCP tools for further detail
- `get_hook_info("useGetStripeConnectStatusQuery")`
- `get_hook_info("useStartStripeConnectOnboardingMutation")`
- `get_hook_info("useGetStripeConnectDashboardQuery")`
- `get_hook_info("useLazyGetStripeConnectDashboardQuery")`
- `get_component_info("MonetizationTab")`
- `get_component_info("StripeConnect")` (the auto-mounted child card)
## Files in this skill's scope
Frontend SDK (`iblai/ibl-web-frontend` on `main`):
- `packages/web-containers/src/components/profile/monetization/stripe-connect.tsx` — the auto-rendered status card
- `packages/web-containers/src/components/profile/monetization/index.tsx` — `MonetizationTab` that mounts the card behind `is_owner`
- `packages/data-layer/src/features/monetization/custom-api-slice.ts` — the four Connect hooks live in `// Flow 1: Stripe Connect`
- `packages/data-layer/src/features/monetization/types.ts` — `StripeConnectStatusResponse`, `StripeConnectOnboardArgs`, `StripeConnectOnboardResponse`, `StripeConnectDashboardResponse`
Backend (`ibl-dm-pro` repo, `dl_iblai_services_app`):
- `views/stripe_connect.py` — `StripeConnectOnboardView` (line 35), `StripeConnectStatusView` (line 92), `StripeConnectDashboardLinkView` (line 119), `StripeConnectDisconnectView` (line 155)
- `models/stripe_connect.py` — `StripeConnectAccount` model, the `is_ready_for_payments` and `commission_percent` properties
- `services/stripe/connect.py` — `StripeConnectService.get_account_status()`, the source of `is_owner` on the response
## Related skills
- `/iblai-vibe-monetization` — family overview + the schema-validation routine
- `/iblai-vibe-monetization-configure` — what to build right after this: the `MonetizationTab` wizard for paywall + price CRUD (depends on `is_ready_for_payments === true`)
- `/iblai-vibe-monetization-checkout` — the buyer-side `PaywallModal` and Stripe checkout that this onboarding enables
- `/iblai-vibe-account` — hosts the `MonetizationTab` and supplies the `authURL` prop
- `/iblai-vibe-auth` — the token wiring every Connect call inherits
- `/iblai-vibe-rbac` — `IsPlatformAdmin` and Platform-owner gating
- [BRAND.md](https://raw.githubusercontent.com/iblai/vibe/refs/heads/main/BRAND.md) — visual conventions for any custom Connect surface you build
Files in this skill
- SKILL.md
- references/connect-api.md
Attribution
Comments
Loading comments…
Newsletter
Get the weekly list of safe new skills, and the ones we pulled.