@dodopayments/opencode-plugin 0.6.0 → 0.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/README.md +1 -0
  2. package/opencode-plugin/README.md +1 -1
  3. package/package.json +1 -1
  4. package/skills/better-auth-integration/SKILL.md +12 -3
  5. package/skills/billing-sdk/SKILL.md +15 -167
  6. package/skills/billing-sdk/references/pricing-page.md +197 -0
  7. package/skills/checkout-integration/SKILL.md +31 -460
  8. package/skills/checkout-integration/references/overlay-checkout.md +148 -0
  9. package/skills/checkout-integration/references/server-examples.md +317 -0
  10. package/skills/credit-based-billing/SKILL.md +11 -158
  11. package/skills/credit-based-billing/references/ai-token-example.md +108 -0
  12. package/skills/credit-based-billing/references/credit-webhooks.md +51 -0
  13. package/skills/customer-management/SKILL.md +25 -17
  14. package/skills/discounts-and-promotions/SKILL.md +20 -17
  15. package/skills/dodo-best-practices/SKILL.md +18 -5
  16. package/skills/dodo-mcp-usage/SKILL.md +152 -0
  17. package/skills/framework-adapters/SKILL.md +64 -421
  18. package/skills/framework-adapters/references/astro.md +52 -0
  19. package/skills/framework-adapters/references/bun.md +45 -0
  20. package/skills/framework-adapters/references/convex.md +91 -0
  21. package/skills/framework-adapters/references/express.md +72 -0
  22. package/skills/framework-adapters/references/fastify.md +54 -0
  23. package/skills/framework-adapters/references/hono.md +63 -0
  24. package/skills/framework-adapters/references/nuxt.md +53 -0
  25. package/skills/framework-adapters/references/remix.md +65 -0
  26. package/skills/framework-adapters/references/sveltekit.md +49 -0
  27. package/skills/framework-adapters/references/tanstack-start.md +31 -0
  28. package/skills/license-keys/SKILL.md +117 -367
  29. package/skills/license-keys/references/cli-tool.md +143 -0
  30. package/skills/license-keys/references/desktop-app.md +170 -0
  31. package/skills/localized-pricing/SKILL.md +51 -29
  32. package/skills/mobile-checkout/SKILL.md +72 -310
  33. package/skills/mobile-checkout/references/android.md +69 -0
  34. package/skills/mobile-checkout/references/flutter.md +97 -0
  35. package/skills/mobile-checkout/references/ios.md +66 -0
  36. package/skills/mobile-checkout/references/react-native.md +100 -0
  37. package/skills/product-catalog-management/SKILL.md +39 -16
  38. package/skills/refunds-and-disputes/SKILL.md +91 -71
  39. package/skills/subscription-integration/SKILL.md +43 -64
  40. package/skills/subscription-integration/references/webhook-events.md +82 -0
  41. package/skills/testing-and-go-live/SKILL.md +22 -15
  42. package/skills/usage-based-billing/SKILL.md +16 -122
  43. package/skills/usage-based-billing/references/instrumentation.md +119 -0
  44. package/skills/webhook-integration/SKILL.md +31 -263
  45. package/skills/webhook-integration/references/common-mistakes.md +146 -0
  46. package/skills/webhook-integration/references/event-catalog.md +103 -0
  47. package/skills/webhook-integration/references/local-testing.md +42 -0
package/README.md CHANGED
@@ -175,6 +175,7 @@ If you prefer the local stdio API server with your own API key instead of the de
175
175
  | `dodo-best-practices` | SDK setup, environments, API keys, and the canonical checkout-to-webhook architecture |
176
176
  | `framework-adapters` | Official `@dodopayments/*` handlers for Next.js, Express, Hono, Astro, Remix, SvelteKit, Nuxt, Fastify, TanStack, Bun, Convex |
177
177
  | `testing-and-go-live` | Test mode, test payment methods, webhook testing, production launch checklist |
178
+ | `dodo-mcp-usage` | When to use the documentation vs API MCP servers, test-mode safety rules, auth, troubleshooting |
178
179
 
179
180
  **Accepting payments**
180
181
 
@@ -37,7 +37,7 @@ opencode run "List every skill available to you by name."
37
37
 
38
38
  ## What you get
39
39
 
40
- Agent skills (auto-loaded when relevant): `dodo-best-practices`, `framework-adapters`, `testing-and-go-live`, `checkout-integration`, `subscription-integration`, `mobile-checkout`, `webhook-integration`, `credit-based-billing`, `usage-based-billing`, `license-keys`, `product-catalog-management`, `discounts-and-promotions`, `localized-pricing`, `customer-management`, `refunds-and-disputes`, `billing-sdk`, `better-auth-integration`.
40
+ Agent skills (auto-loaded when relevant): `dodo-best-practices`, `dodo-mcp-usage`, `framework-adapters`, `testing-and-go-live`, `checkout-integration`, `subscription-integration`, `mobile-checkout`, `webhook-integration`, `credit-based-billing`, `usage-based-billing`, `license-keys`, `product-catalog-management`, `discounts-and-promotions`, `localized-pricing`, `customer-management`, `refunds-and-disputes`, `billing-sdk`, `better-auth-integration`.
41
41
 
42
42
  Two MCP servers (registered automatically):
43
43
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dodopayments/opencode-plugin",
3
- "version": "0.6.0",
3
+ "version": "0.6.2",
4
4
  "description": "Dodo Payments agent skills and MCP servers for OpenCode.",
5
5
  "type": "module",
6
6
  "main": "opencode-plugin/index.js",
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: better-auth-integration
3
- description: Guide only for applications using @dodopayments/better-auth, covering authenticated customer sync, checkout, portal access, usage ingestion, and verified webhook callbacks.
3
+ description: Dodo Payments plugin for Better Auth (@dodopayments/better-auth) covering customer sync on signup, authenticated checkout, customer portal, usage ingestion, and verified webhooks. Use when an app uses Better Auth and needs the dodopayments, dodopaymentsClient, checkout, portal, usage, or webhooks plugins; for other auth stacks use framework-adapters.
4
4
  ---
5
5
 
6
6
  # Better Auth Integration
@@ -18,9 +18,11 @@ Use `@dodopayments/better-auth` to synchronize Better Auth users with Dodo Payme
18
18
  ## Install
19
19
 
20
20
  ```bash
21
- npm install @dodopayments/better-auth dodopayments better-auth
21
+ npm install @dodopayments/better-auth dodopayments better-auth zod
22
22
  ```
23
23
 
24
+ Requires Better Auth 1.4 or a later 1.x release.
25
+
24
26
  The package exports the lowercase server functions `dodopayments`, `checkout`, `portal`, `usage`, and `webhooks`. Its client entry point exports `dodopaymentsClient`.
25
27
 
26
28
  ## Server setup
@@ -76,6 +78,13 @@ export const auth = betterAuth({
76
78
 
77
79
  `products` maps an application slug to a Dodo product ID. `authenticatedUsersOnly: true` requires a Better Auth session for checkout. `getCustomerParams` adds application metadata when customer synchronization runs.
78
80
 
81
+ The plugin adds a `dodoCustomerId` field to the Better Auth `user` table. After adding the plugin, update your database schema with the [Better Auth CLI](https://www.better-auth.com/docs/concepts/cli):
82
+
83
+ ```bash
84
+ npx auth generate # generate the ORM schema or SQL migration, then apply it yourself
85
+ npx auth migrate # or apply it directly (built-in Kysely adapter only)
86
+ ```
87
+
79
88
  ## Browser client setup
80
89
 
81
90
  ```typescript
@@ -170,7 +179,7 @@ if (error) {
170
179
  }
171
180
  ```
172
181
 
173
- The plugin derives `customer_id` from the authenticated session. `event_id` is the event's idempotency key; reuse it when retrying the same event.
182
+ Both `usage.ingest` and `usage.meters.list` require a signed-in user with a verified email address. The plugin derives `customer_id` from the authenticated session. `event_id` is the event's idempotency key; reuse it when retrying the same event.
174
183
 
175
184
  ## Webhook endpoint
176
185
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: billing-sdk
3
- description: Guide for building billing UI with BillingSDK - the open-source React component library for pricing tables, subscription management, usage meters, invoice history, and customer portal flows wired to Dodo Payments.
3
+ description: BillingSDK, the open-source React and shadcn component library for Dodo Payments billing UI. Use when building pricing tables, pricing pages, account billing pages, usage meters, credit balances, invoice history, or cancel and upgrade UI in React or Next.js, or running @billingsdk/cli init; keep Dodo API calls server-side.
4
4
  ---
5
5
 
6
6
  # BillingSDK
@@ -75,8 +75,11 @@ From the current official registry, grouped by purpose:
75
75
  | Payments | `payment-details`, `payment-details-two`, `payment-method-selector`, `payment-card`, `payment-failure` |
76
76
  | Promotion and trials | `banner`, `limited-offer-dialog`, `trial-expiry-card` |
77
77
 
78
- `pricing-table-one` is the only block with an officially published prop example, reproduced below. For
79
- every other block, run `add` and read the generated file's props type.
78
+ Most blocks have a published usage example and props table on their page at
79
+ [billingsdk.com/docs/components](https://billingsdk.com/docs/components) (for example `subscription-management`
80
+ and `billing-settings`); `pricing-table-one` is reproduced in
81
+ [references/pricing-page.md](references/pricing-page.md). The generated file's props type is still
82
+ authoritative for the version you installed, so read it after running `add`.
80
83
 
81
84
  ## Server client
82
85
 
@@ -101,169 +104,14 @@ are `https://test.dodopayments.com` and `https://live.dodopayments.com`.
101
104
 
102
105
  ## Pricing page
103
106
 
104
- ### 1. Plan catalogue
107
+ Full guide: [references/pricing-page.md](references/pricing-page.md).
105
108
 
106
- Keep the product ids and the display copy in one module. The `price` here is a display number rendered by
107
- the component — the amount actually charged comes from the Dodo product catalogue, and Dodo API amounts
108
- are always in the smallest currency unit (cents). Treat this file as a mirror of the dashboard and
109
- reconcile it when you change a price.
109
+ Covers:
110
110
 
111
- ```typescript
112
- // lib/plans.ts
113
- export type PlanId = 'starter' | 'pro';
114
-
115
- export interface Plan {
116
- id: PlanId;
117
- title: string;
118
- price: number;
119
- period: string;
120
- features: string[];
121
- popular: boolean;
122
- }
123
-
124
- export const PLANS: readonly Plan[] = [
125
- {
126
- id: 'starter',
127
- title: 'Starter',
128
- price: 9,
129
- period: 'month',
130
- features: ['100 requests', 'Basic support', '1 project'],
131
- popular: false,
132
- },
133
- {
134
- id: 'pro',
135
- title: 'Pro',
136
- price: 29,
137
- period: 'month',
138
- features: ['Unlimited requests', 'Priority support', '10 projects'],
139
- popular: true,
140
- },
141
- ];
142
- ```
143
-
144
- ### 2. Product id map — server only
145
-
146
- Never let the browser choose an arbitrary `product_id`. Map the public plan slug to a `pdt_` id on the
147
- server and reject anything else.
148
-
149
- ```typescript
150
- // lib/plan-products.server.ts
151
- import 'server-only';
152
- import type { PlanId } from './plans';
153
-
154
- const PRODUCT_IDS: Record<PlanId, string> = {
155
- starter: 'pdt_starter_monthly',
156
- pro: 'pdt_pro_monthly',
157
- };
158
-
159
- export function productIdForPlan(value: unknown): string | null {
160
- if (typeof value !== 'string') return null;
161
- return PRODUCT_IDS[value as PlanId] ?? null;
162
- }
163
- ```
164
-
165
- ### 3. Checkout route
166
-
167
- ```typescript
168
- // app/api/checkout/route.ts
169
- import { NextResponse } from 'next/server';
170
- import { dodo } from '@/lib/dodopayments';
171
- import { productIdForPlan } from '@/lib/plan-products.server';
172
- import { getCurrentUser } from '@/lib/auth';
173
-
174
- export async function POST(request: Request) {
175
- const user = await getCurrentUser();
176
- if (!user) {
177
- return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });
178
- }
179
-
180
- const body: unknown = await request.json();
181
- const planId = (body as { planId?: unknown }).planId;
182
- const productId = productIdForPlan(planId);
183
-
184
- if (!productId) {
185
- return NextResponse.json({ error: 'Unknown plan' }, { status: 400 });
186
- }
187
-
188
- try {
189
- const session = await dodo.checkoutSessions.create({
190
- product_cart: [{ product_id: productId, quantity: 1 }],
191
- customer: { email: user.email, name: user.name },
192
- return_url: `${process.env.NEXT_PUBLIC_APP_URL}/billing/return`,
193
- cancel_url: `${process.env.NEXT_PUBLIC_APP_URL}/pricing`,
194
- metadata: { app_user_id: user.id },
195
- });
196
-
197
- if (!session.checkout_url) {
198
- return NextResponse.json({ error: 'No checkout URL returned' }, { status: 502 });
199
- }
200
-
201
- return NextResponse.json({ checkoutUrl: session.checkout_url });
202
- } catch (error) {
203
- console.error('Checkout session creation failed', error);
204
- return NextResponse.json({ error: 'Could not start checkout' }, { status: 500 });
205
- }
206
- }
207
- ```
208
-
209
- `checkout_url` is nullable — it is absent when the session is created with `payment_method_id` — so guard
210
- it rather than redirecting to `undefined`. Checkout URLs are single-use and normally expire after 24 hours.
211
-
212
- Use `checkoutSessions.create`. `payments.create` and `subscriptions.create` are deprecated for new
213
- integrations. Checkout parameters are covered in depth in the `checkout-integration` skill.
214
-
215
- ### 4. Client component
216
-
217
- ```tsx
218
- // components/pricing-plans.tsx
219
- 'use client';
220
-
221
- import { useState } from 'react';
222
- import { PricingTableOne } from '@/components/billingsdk/pricing-table-one';
223
- import { PLANS } from '@/lib/plans';
224
-
225
- export function PricingPlans() {
226
- const [error, setError] = useState<string | null>(null);
227
-
228
- async function handlePlanSelect(planId: string) {
229
- setError(null);
230
- try {
231
- const response = await fetch('/api/checkout', {
232
- method: 'POST',
233
- headers: { 'Content-Type': 'application/json' },
234
- body: JSON.stringify({ planId }),
235
- });
236
-
237
- if (!response.ok) {
238
- setError('Could not start checkout. Please try again.');
239
- return;
240
- }
241
-
242
- const { checkoutUrl } = (await response.json()) as { checkoutUrl: string };
243
- window.location.href = checkoutUrl;
244
- } catch {
245
- setError('Network error. Please try again.');
246
- }
247
- }
248
-
249
- return (
250
- <>
251
- <PricingTableOne
252
- plans={PLANS}
253
- title="Choose your plan"
254
- description="Select the plan that works best for you"
255
- onPlanSelect={handlePlanSelect}
256
- theme="classic"
257
- size="medium"
258
- />
259
- {error ? <p role="alert">{error}</p> : null}
260
- </>
261
- );
262
- }
263
- ```
264
-
265
- Render `<PricingPlans />` from `app/pricing/page.tsx`. Nothing secret crosses into the client component:
266
- it sends a plan slug and receives a URL.
111
+ - Plan catalogue
112
+ - Product id map — server only
113
+ - Checkout route
114
+ - Client component
267
115
 
268
116
  ## Subscription management and customer portal
269
117
 
@@ -458,14 +306,14 @@ signature verification is covered in the `webhook-integration` skill.
458
306
  **Accepting a raw `product_id` from the request body.** A caller can then check out against any product in
459
307
  your catalogue, including internal or discounted ones. Map an opaque plan slug to a `pdt_` id server-side.
460
308
 
461
- **Hardcoding prices in the client.** `PLANS[].price` is display text. When you change a price in the Dodo
309
+ **Hardcoding prices in the client.** `plans[].monthlyPrice` / `yearlyPrice` are display text. When you change a price in the Dodo
462
310
  dashboard, the checkout charges the new amount while the pricing page keeps advertising the old one.
463
- Reconcile `lib/plans.ts` with the dashboard as part of any pricing change.
311
+ Reconcile `lib/billingsdk-config.ts` with the dashboard as part of any pricing change.
464
312
 
465
313
  **`npm install billingsdk`.** The root repository package is private. Install through
466
314
  `@billingsdk/cli` or the shadcn registry instead.
467
315
 
468
- **Guessing prop names.** `pricing-table-one` is the only block with a published prop example. Open the
316
+ **Guessing prop names.** Check the block's page on billingsdk.com/docs/components, then open the
469
317
  generated file under `components/billingsdk/` and read the exported props type before wiring anything else.
470
318
 
471
319
  **Using `DODO_PAYMENTS_WEBHOOK_SECRET`.** The SDK reads `DODO_PAYMENTS_WEBHOOK_KEY` for the `webhookKey`
@@ -0,0 +1,197 @@
1
+ # BillingSDK: Pricing page
2
+
3
+
4
+ ### 1. Plan catalogue
5
+
6
+ `pricing-table-one` takes BillingSDK's `Plan[]`, not an arbitrary shape. Adding the component generates
7
+ `lib/billingsdk-config.ts`, which exports the `Plan` interface and a sample `plans` array — edit that array
8
+ rather than defining your own type. Prices are display **strings** (`monthlyPrice`, `yearlyPrice`) and
9
+ `features` are objects. The amount actually charged comes from the Dodo product catalogue (always in the
10
+ smallest currency unit), so treat this file as a mirror of the dashboard and reconcile it when you change a
11
+ price.
12
+
13
+ ```typescript
14
+ // lib/billingsdk-config.ts (generated by `add pricing-table-one`; edit the plans)
15
+ export interface Plan {
16
+ id: string;
17
+ title: string;
18
+ description: string;
19
+ highlight?: boolean;
20
+ type?: 'monthly' | 'yearly';
21
+ currency?: string;
22
+ monthlyPrice: string;
23
+ yearlyPrice: string;
24
+ buttonText: string;
25
+ badge?: string;
26
+ features: {
27
+ name: string;
28
+ icon: string;
29
+ iconColor?: string;
30
+ }[];
31
+ }
32
+
33
+ export const plans: Plan[] = [
34
+ {
35
+ id: 'starter',
36
+ title: 'Starter',
37
+ description: 'For individuals getting started.',
38
+ currency: '$',
39
+ monthlyPrice: '9',
40
+ yearlyPrice: '90',
41
+ buttonText: 'Choose Starter',
42
+ features: [
43
+ { name: '100 requests', icon: 'check' },
44
+ { name: 'Basic support', icon: 'check' },
45
+ { name: '1 project', icon: 'check' },
46
+ ],
47
+ },
48
+ {
49
+ id: 'pro',
50
+ title: 'Pro',
51
+ description: 'For teams running in production.',
52
+ currency: '$',
53
+ monthlyPrice: '29',
54
+ yearlyPrice: '290',
55
+ buttonText: 'Choose Pro',
56
+ badge: 'Most popular',
57
+ highlight: true,
58
+ features: [
59
+ { name: 'Unlimited requests', icon: 'check' },
60
+ { name: 'Priority support', icon: 'check' },
61
+ { name: '10 projects', icon: 'check' },
62
+ ],
63
+ },
64
+ ];
65
+ ```
66
+
67
+ The generated file is the contract for your installed version; if its `Plan` differs from the above, follow
68
+ the file. `onPlanSelect` receives only the plan `id`, so the ids here are the slugs the server maps below.
69
+
70
+ This example sells **monthly plans only**: `yearlyPrice` is required by the `Plan` type and is displayed,
71
+ but the server map below has only monthly products. To sell yearly too, create the yearly products in Dodo,
72
+ track the table's billing-period toggle in your own state (it is not passed to `onPlanSelect`), send it with
73
+ the plan id, and key the server map by plan and period (for example `pro:yearly`).
74
+
75
+ ### 2. Product id map — server only
76
+
77
+ Never let the browser choose an arbitrary `product_id`. Map the public plan slug to a `pdt_` id on the
78
+ server and reject anything else.
79
+
80
+ ```typescript
81
+ // lib/plan-products.server.ts
82
+ import 'server-only';
83
+
84
+ // Keys must match the `id`s in lib/billingsdk-config.ts.
85
+ const PRODUCT_IDS: Readonly<Record<string, string>> = {
86
+ starter: 'pdt_starter_monthly',
87
+ pro: 'pdt_pro_monthly',
88
+ };
89
+
90
+ export function productIdForPlan(value: unknown): string | null {
91
+ if (typeof value !== 'string' || !Object.hasOwn(PRODUCT_IDS, value)) return null;
92
+ return PRODUCT_IDS[value];
93
+ }
94
+ ```
95
+
96
+ ### 3. Checkout route
97
+
98
+ ```typescript
99
+ // app/api/checkout/route.ts
100
+ import { NextResponse } from 'next/server';
101
+ import { dodo } from '@/lib/dodopayments';
102
+ import { productIdForPlan } from '@/lib/plan-products.server';
103
+ import { getCurrentUser } from '@/lib/auth';
104
+
105
+ export async function POST(request: Request) {
106
+ const user = await getCurrentUser();
107
+ if (!user) {
108
+ return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });
109
+ }
110
+
111
+ const body: unknown = await request.json();
112
+ const planId = (body as { planId?: unknown }).planId;
113
+ const productId = productIdForPlan(planId);
114
+
115
+ if (!productId) {
116
+ return NextResponse.json({ error: 'Unknown plan' }, { status: 400 });
117
+ }
118
+
119
+ try {
120
+ const session = await dodo.checkoutSessions.create({
121
+ product_cart: [{ product_id: productId, quantity: 1 }],
122
+ customer: { email: user.email, name: user.name },
123
+ return_url: `${process.env.NEXT_PUBLIC_APP_URL}/billing/return`,
124
+ cancel_url: `${process.env.NEXT_PUBLIC_APP_URL}/pricing`,
125
+ metadata: { app_user_id: user.id },
126
+ });
127
+
128
+ if (!session.checkout_url) {
129
+ return NextResponse.json({ error: 'No checkout URL returned' }, { status: 502 });
130
+ }
131
+
132
+ return NextResponse.json({ checkoutUrl: session.checkout_url });
133
+ } catch (error) {
134
+ console.error('Checkout session creation failed', error);
135
+ return NextResponse.json({ error: 'Could not start checkout' }, { status: 500 });
136
+ }
137
+ }
138
+ ```
139
+
140
+ `checkout_url` is nullable — it is absent when the session is created with `payment_method_id` — so guard
141
+ it rather than redirecting to `undefined`. Checkout URLs are single-use and normally expire after 24 hours.
142
+
143
+ Use `checkoutSessions.create`. `payments.create` and `subscriptions.create` are deprecated for new
144
+ integrations. Checkout parameters are covered in depth in the `checkout-integration` skill.
145
+
146
+ ### 4. Client component
147
+
148
+ ```tsx
149
+ // components/pricing-plans.tsx
150
+ 'use client';
151
+
152
+ import { useState } from 'react';
153
+ import { PricingTableOne } from '@/components/billingsdk/pricing-table-one';
154
+ import { plans } from '@/lib/billingsdk-config';
155
+
156
+ export function PricingPlans() {
157
+ const [error, setError] = useState<string | null>(null);
158
+
159
+ async function handlePlanSelect(planId: string) {
160
+ setError(null);
161
+ try {
162
+ const response = await fetch('/api/checkout', {
163
+ method: 'POST',
164
+ headers: { 'Content-Type': 'application/json' },
165
+ body: JSON.stringify({ planId }),
166
+ });
167
+
168
+ if (!response.ok) {
169
+ setError('Could not start checkout. Please try again.');
170
+ return;
171
+ }
172
+
173
+ const { checkoutUrl } = (await response.json()) as { checkoutUrl: string };
174
+ window.location.href = checkoutUrl;
175
+ } catch {
176
+ setError('Network error. Please try again.');
177
+ }
178
+ }
179
+
180
+ return (
181
+ <>
182
+ <PricingTableOne
183
+ plans={plans}
184
+ title="Choose your plan"
185
+ description="Select the plan that works best for you"
186
+ onPlanSelect={handlePlanSelect}
187
+ theme="classic"
188
+ size="medium"
189
+ />
190
+ {error ? <p role="alert">{error}</p> : null}
191
+ </>
192
+ );
193
+ }
194
+ ```
195
+
196
+ Render `<PricingPlans />` from `app/pricing/page.tsx`. Nothing secret crosses into the client component:
197
+ it sends a plan slug and receives a URL.