@dodopayments/opencode-plugin 0.6.1 → 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 (36) hide show
  1. package/package.json +1 -1
  2. package/skills/billing-sdk/SKILL.md +8 -193
  3. package/skills/billing-sdk/references/pricing-page.md +197 -0
  4. package/skills/checkout-integration/SKILL.md +14 -449
  5. package/skills/checkout-integration/references/overlay-checkout.md +148 -0
  6. package/skills/checkout-integration/references/server-examples.md +317 -0
  7. package/skills/credit-based-billing/SKILL.md +5 -151
  8. package/skills/credit-based-billing/references/ai-token-example.md +108 -0
  9. package/skills/credit-based-billing/references/credit-webhooks.md +51 -0
  10. package/skills/framework-adapters/SKILL.md +38 -527
  11. package/skills/framework-adapters/references/astro.md +52 -0
  12. package/skills/framework-adapters/references/bun.md +45 -0
  13. package/skills/framework-adapters/references/convex.md +91 -0
  14. package/skills/framework-adapters/references/express.md +72 -0
  15. package/skills/framework-adapters/references/fastify.md +54 -0
  16. package/skills/framework-adapters/references/hono.md +63 -0
  17. package/skills/framework-adapters/references/nuxt.md +53 -0
  18. package/skills/framework-adapters/references/remix.md +65 -0
  19. package/skills/framework-adapters/references/sveltekit.md +49 -0
  20. package/skills/framework-adapters/references/tanstack-start.md +31 -0
  21. package/skills/license-keys/SKILL.md +8 -303
  22. package/skills/license-keys/references/cli-tool.md +143 -0
  23. package/skills/license-keys/references/desktop-app.md +170 -0
  24. package/skills/mobile-checkout/SKILL.md +25 -313
  25. package/skills/mobile-checkout/references/android.md +69 -0
  26. package/skills/mobile-checkout/references/flutter.md +97 -0
  27. package/skills/mobile-checkout/references/ios.md +66 -0
  28. package/skills/mobile-checkout/references/react-native.md +100 -0
  29. package/skills/subscription-integration/SKILL.md +3 -77
  30. package/skills/subscription-integration/references/webhook-events.md +82 -0
  31. package/skills/usage-based-billing/SKILL.md +5 -114
  32. package/skills/usage-based-billing/references/instrumentation.md +119 -0
  33. package/skills/webhook-integration/SKILL.md +28 -277
  34. package/skills/webhook-integration/references/common-mistakes.md +146 -0
  35. package/skills/webhook-integration/references/event-catalog.md +103 -0
  36. package/skills/webhook-integration/references/local-testing.md +42 -0
@@ -347,85 +347,11 @@ For full customer management (creating, updating, listing), see the `customer-ma
347
347
 
348
348
  ## Webhook Events
349
349
 
350
- | Event | When | Action |
351
- |-------|------|--------|
352
- | `subscription.active` | Subscription becomes active, including a trial start or recovery | Grant access |
353
- | `subscription.updated` | Any subscription field changes | Sync your stored copy (status, dates, metadata) |
354
- | `subscription.renewed` | Successful renewal | Log renewal, send receipt |
355
- | `subscription.past_due` | Renewal failed and the grace period opened (payload has `past_due_ends_at`) | Keep access, prompt for payment before the deadline |
356
- | `subscription.on_hold` | Renewal/plan-change payment failed | Notify customer, offer recovery |
357
- | `subscription.paused` | Subscription paused | Revoke access while paused |
358
- | `subscription.unpaused` | Paused subscription resumed | Restore access |
359
- | `subscription.plan_changed` | Plan upgraded/downgraded or add-ons changed | Update entitlements |
360
- | `subscription.update_payment_method` | Payment method updated | Update stored payment details |
361
- | `subscription.cancelled` | Customer cancels | Schedule access revocation per `cancel_at_next_billing_date` |
362
- | `subscription.failed` | Initial creation failed (mandate creation) | Notify customer, offer a new subscription |
363
- | `subscription.expired` | Subscription term ended | Revoke access |
364
-
365
- Webhook signature verification, raw-body handling, durable processing, and idempotency are covered in the `webhook-integration` skill.
366
-
367
- ### Example Handler
350
+ Full guide: [references/webhook-events.md](references/webhook-events.md).
368
351
 
369
- ```typescript
370
- import { NextRequest, NextResponse } from 'next/server';
371
-
372
- export async function POST(req: NextRequest) {
373
- const raw = await req.text();
374
- const headers = Object.fromEntries(req.headers.entries());
375
- const event = await client.webhooks.unwrap(raw, { headers });
376
- const webhookId = req.headers.get('webhook-id');
377
-
378
- if (!webhookId) {
379
- return NextResponse.json({ error: 'Missing webhook-id' }, { status: 400 });
380
- }
381
-
382
- // Implement this as an atomic insert backed by a UNIQUE constraint.
383
- // Keep the claim and entitlement changes in the same database transaction.
384
- const claimed = await claimWebhookId(webhookId);
385
- if (!claimed) {
386
- return NextResponse.json({ received: true });
387
- }
388
-
389
- switch (event.type) {
390
- case 'subscription.active':
391
- await grantAccess(event.data.customer.customer_id, event.data.product_id);
392
- break;
393
- case 'subscription.on_hold':
394
- await notifyPaymentFailed(event.data.customer.customer_id);
395
- break;
396
- case 'subscription.paused':
397
- case 'subscription.unpaused': {
398
- // Pause and resume can happen in quick succession, and handlers for
399
- // different events can finish in any order. Don't trust the event type:
400
- // re-read the subscription and apply its CURRENT status, scoped to this
401
- // subscription (the customer may hold other active ones). Run this under
402
- // a per-subscription lock if your handlers execute concurrently.
403
- const current = await client.subscriptions.retrieve(event.data.subscription_id);
404
- if (current.status === 'paused') {
405
- await suspendSubscriptionAccess(current.subscription_id);
406
- } else if (current.status === 'active' || current.status === 'past_due') {
407
- // past_due keeps access during the payment grace period
408
- await restoreSubscriptionAccess(current.subscription_id);
409
- }
410
- break;
411
- }
412
- case 'subscription.cancelled':
413
- if (event.data.cancel_at_next_billing_date) {
414
- await scheduleAccessRevocation(event.data.subscription_id, new Date(event.data.next_billing_date));
415
- } else {
416
- await revokeAccessImmediately(event.data.subscription_id);
417
- }
418
- break;
419
- case 'subscription.expired':
420
- await revokeAccess(event.data.customer.customer_id);
421
- break;
422
- }
423
-
424
- return NextResponse.json({ received: true });
425
- }
426
- ```
352
+ Covers:
427
353
 
428
- ---
354
+ - Example Handler
429
355
 
430
356
  ## Credit Entitlements
431
357
 
@@ -0,0 +1,82 @@
1
+ # Dodo Payments Subscription Integration: Webhook Events
2
+
3
+
4
+ | Event | When | Action |
5
+ |-------|------|--------|
6
+ | `subscription.active` | Subscription becomes active, including a trial start or recovery | Grant access |
7
+ | `subscription.updated` | Any subscription field changes | Sync your stored copy (status, dates, metadata) |
8
+ | `subscription.renewed` | Successful renewal | Log renewal, send receipt |
9
+ | `subscription.past_due` | Renewal failed and the grace period opened (payload has `past_due_ends_at`) | Keep access, prompt for payment before the deadline |
10
+ | `subscription.on_hold` | Renewal/plan-change payment failed | Notify customer, offer recovery |
11
+ | `subscription.paused` | Subscription paused | Revoke access while paused |
12
+ | `subscription.unpaused` | Paused subscription resumed | Restore access |
13
+ | `subscription.plan_changed` | Plan upgraded/downgraded or add-ons changed | Update entitlements |
14
+ | `subscription.update_payment_method` | Payment method updated | Update stored payment details |
15
+ | `subscription.cancelled` | Customer cancels | Schedule access revocation per `cancel_at_next_billing_date` |
16
+ | `subscription.failed` | Initial creation failed (mandate creation) | Notify customer, offer a new subscription |
17
+ | `subscription.expired` | Subscription term ended | Revoke access |
18
+
19
+ Webhook signature verification, raw-body handling, durable processing, and idempotency are covered in the `webhook-integration` skill.
20
+
21
+ ### Example Handler
22
+
23
+ ```typescript
24
+ import { NextRequest, NextResponse } from 'next/server';
25
+
26
+ export async function POST(req: NextRequest) {
27
+ const raw = await req.text();
28
+ const headers = Object.fromEntries(req.headers.entries());
29
+ const event = await client.webhooks.unwrap(raw, { headers });
30
+ const webhookId = req.headers.get('webhook-id');
31
+
32
+ if (!webhookId) {
33
+ return NextResponse.json({ error: 'Missing webhook-id' }, { status: 400 });
34
+ }
35
+
36
+ // Implement this as an atomic insert backed by a UNIQUE constraint.
37
+ // Keep the claim and entitlement changes in the same database transaction.
38
+ const claimed = await claimWebhookId(webhookId);
39
+ if (!claimed) {
40
+ return NextResponse.json({ received: true });
41
+ }
42
+
43
+ switch (event.type) {
44
+ case 'subscription.active':
45
+ await grantAccess(event.data.customer.customer_id, event.data.product_id);
46
+ break;
47
+ case 'subscription.on_hold':
48
+ await notifyPaymentFailed(event.data.customer.customer_id);
49
+ break;
50
+ case 'subscription.paused':
51
+ case 'subscription.unpaused': {
52
+ // Pause and resume can happen in quick succession, and handlers for
53
+ // different events can finish in any order. Don't trust the event type:
54
+ // re-read the subscription and apply its CURRENT status, scoped to this
55
+ // subscription (the customer may hold other active ones). Run this under
56
+ // a per-subscription lock if your handlers execute concurrently.
57
+ const current = await client.subscriptions.retrieve(event.data.subscription_id);
58
+ if (current.status === 'paused') {
59
+ await suspendSubscriptionAccess(current.subscription_id);
60
+ } else if (current.status === 'active' || current.status === 'past_due') {
61
+ // past_due keeps access during the payment grace period
62
+ await restoreSubscriptionAccess(current.subscription_id);
63
+ }
64
+ break;
65
+ }
66
+ case 'subscription.cancelled':
67
+ if (event.data.cancel_at_next_billing_date) {
68
+ await scheduleAccessRevocation(event.data.subscription_id, new Date(event.data.next_billing_date));
69
+ } else {
70
+ await revokeAccessImmediately(event.data.subscription_id);
71
+ }
72
+ break;
73
+ case 'subscription.expired':
74
+ await revokeAccess(event.data.customer.customer_id);
75
+ break;
76
+ }
77
+
78
+ return NextResponse.json({ received: true });
79
+ }
80
+ ```
81
+
82
+ ---
@@ -235,122 +235,13 @@ The `product-catalog-management` skill is the canonical source for the complete
235
235
 
236
236
  ## Instrumenting Your Application
237
237
 
238
- ### Track API Calls
238
+ Full guide: [references/instrumentation.md](references/instrumentation.md).
239
239
 
240
- Persist the event before reporting the operation as complete, then ingest it from a retrying worker. The outbox or queue implementation must durably store the payload before `persist` resolves.
240
+ Covers:
241
241
 
242
- ```typescript
243
- type PersistedUsageEvent = {
244
- event_id: string;
245
- customer_id: string;
246
- event_name: string;
247
- timestamp: string;
248
- metadata: Record<string, string | number | boolean>;
249
- };
250
-
251
- interface UsageOutbox {
252
- persist(event: PersistedUsageEvent): Promise<void>;
253
- nextBatch(limit: number): Promise<PersistedUsageEvent[]>;
254
- markIngested(eventIds: string[]): Promise<void>;
255
- }
256
-
257
- async function completeApiOperation(
258
- outbox: UsageOutbox,
259
- operationId: string,
260
- customerId: string,
261
- occurredAt: string,
262
- ): Promise<void> {
263
- await outbox.persist({
264
- event_id: `api-call:${operationId}`,
265
- customer_id: customerId,
266
- event_name: 'api.call',
267
- timestamp: occurredAt,
268
- metadata: { endpoint: '/v1/users', method: 'GET', status: 200 },
269
- });
270
- }
271
-
272
- async function ingestUsageOutbox(outbox: UsageOutbox): Promise<void> {
273
- const events = await outbox.nextBatch(1000);
274
- if (events.length === 0) return;
275
-
276
- await client.usageEvents.ingest({ events });
277
- await outbox.markIngested(events.map((event) => event.event_id));
278
- }
279
- ```
280
-
281
- If the worker crashes after Dodo accepts the batch but before `markIngested`, retry the same persisted events with the same IDs. Dodo ignores the already-ingested IDs.
282
-
283
- ### Track AI Token Usage
284
-
285
- ```typescript
286
- async function callAI(
287
- customerId: string,
288
- generationId: string,
289
- prompt: string,
290
- completedAt: string,
291
- ) {
292
- const response = await openai.chat.completions.create({
293
- model: 'gpt-4',
294
- messages: [{ role: 'user', content: prompt }],
295
- });
296
-
297
- // Track tokens after completion
298
- await client.usageEvents.ingest({
299
- events: [{
300
- event_id: `generation:${generationId}`,
301
- customer_id: customerId,
302
- event_name: 'ai.tokens',
303
- timestamp: completedAt,
304
- // Aggregated properties (Sum/Max/Last) must be numbers, not strings.
305
- metadata: {
306
- tokens: response.usage.total_tokens,
307
- prompt_tokens: response.usage.prompt_tokens,
308
- completion_tokens: response.usage.completion_tokens,
309
- model: 'gpt-4',
310
- }
311
- }]
312
- });
313
-
314
- return response;
315
- }
316
- ```
317
-
318
- ### Track Storage Usage
319
-
320
- For snapshot-based metrics (current state), use the `last` aggregation:
321
-
322
- ```typescript
323
- async function updateStorageUsage(
324
- customerId: string,
325
- snapshotId: string,
326
- bytesUsed: number,
327
- capturedAt: string,
328
- ) {
329
- await client.usageEvents.ingest({
330
- events: [{
331
- event_id: `storage-snapshot:${snapshotId}`,
332
- customer_id: customerId,
333
- event_name: 'storage.snapshot',
334
- timestamp: capturedAt,
335
- // Numeric values: a string property does not aggregate.
336
- metadata: {
337
- bytes: bytesUsed,
338
- gb: Number((bytesUsed / 1024 / 1024 / 1024).toFixed(2)),
339
- }
340
- }]
341
- });
342
- }
343
-
344
- // Call periodically or after storage changes
345
- await updateStorageUsage(
346
- 'cus_abc',
347
- 'snapshot_01K1M4D2K9',
348
- 5368709120,
349
- '2026-08-01T10:30:00Z',
350
- ); // 5GB
351
- ```
352
-
353
- ---
242
+ - Track API Calls
243
+ - Track AI Token Usage
244
+ - Track Storage Usage
354
245
 
355
246
  ## Querying Usage for Display
356
247
 
@@ -0,0 +1,119 @@
1
+ # Dodo Payments Usage-Based Billing: Instrumenting Your Application
2
+
3
+
4
+ ### Track API Calls
5
+
6
+ Persist the event before reporting the operation as complete, then ingest it from a retrying worker. The outbox or queue implementation must durably store the payload before `persist` resolves.
7
+
8
+ ```typescript
9
+ type PersistedUsageEvent = {
10
+ event_id: string;
11
+ customer_id: string;
12
+ event_name: string;
13
+ timestamp: string;
14
+ metadata: Record<string, string | number | boolean>;
15
+ };
16
+
17
+ interface UsageOutbox {
18
+ persist(event: PersistedUsageEvent): Promise<void>;
19
+ nextBatch(limit: number): Promise<PersistedUsageEvent[]>;
20
+ markIngested(eventIds: string[]): Promise<void>;
21
+ }
22
+
23
+ async function completeApiOperation(
24
+ outbox: UsageOutbox,
25
+ operationId: string,
26
+ customerId: string,
27
+ occurredAt: string,
28
+ ): Promise<void> {
29
+ await outbox.persist({
30
+ event_id: `api-call:${operationId}`,
31
+ customer_id: customerId,
32
+ event_name: 'api.call',
33
+ timestamp: occurredAt,
34
+ metadata: { endpoint: '/v1/users', method: 'GET', status: 200 },
35
+ });
36
+ }
37
+
38
+ async function ingestUsageOutbox(outbox: UsageOutbox): Promise<void> {
39
+ const events = await outbox.nextBatch(1000);
40
+ if (events.length === 0) return;
41
+
42
+ await client.usageEvents.ingest({ events });
43
+ await outbox.markIngested(events.map((event) => event.event_id));
44
+ }
45
+ ```
46
+
47
+ If the worker crashes after Dodo accepts the batch but before `markIngested`, retry the same persisted events with the same IDs. Dodo ignores the already-ingested IDs.
48
+
49
+ ### Track AI Token Usage
50
+
51
+ ```typescript
52
+ async function callAI(
53
+ customerId: string,
54
+ generationId: string,
55
+ prompt: string,
56
+ completedAt: string,
57
+ ) {
58
+ const response = await openai.chat.completions.create({
59
+ model: 'gpt-4',
60
+ messages: [{ role: 'user', content: prompt }],
61
+ });
62
+
63
+ // Track tokens after completion
64
+ await client.usageEvents.ingest({
65
+ events: [{
66
+ event_id: `generation:${generationId}`,
67
+ customer_id: customerId,
68
+ event_name: 'ai.tokens',
69
+ timestamp: completedAt,
70
+ // Aggregated properties (Sum/Max/Last) must be numbers, not strings.
71
+ metadata: {
72
+ tokens: response.usage.total_tokens,
73
+ prompt_tokens: response.usage.prompt_tokens,
74
+ completion_tokens: response.usage.completion_tokens,
75
+ model: 'gpt-4',
76
+ }
77
+ }]
78
+ });
79
+
80
+ return response;
81
+ }
82
+ ```
83
+
84
+ ### Track Storage Usage
85
+
86
+ For snapshot-based metrics (current state), use the `last` aggregation:
87
+
88
+ ```typescript
89
+ async function updateStorageUsage(
90
+ customerId: string,
91
+ snapshotId: string,
92
+ bytesUsed: number,
93
+ capturedAt: string,
94
+ ) {
95
+ await client.usageEvents.ingest({
96
+ events: [{
97
+ event_id: `storage-snapshot:${snapshotId}`,
98
+ customer_id: customerId,
99
+ event_name: 'storage.snapshot',
100
+ timestamp: capturedAt,
101
+ // Numeric values: a string property does not aggregate.
102
+ metadata: {
103
+ bytes: bytesUsed,
104
+ gb: Number((bytesUsed / 1024 / 1024 / 1024).toFixed(2)),
105
+ }
106
+ }]
107
+ });
108
+ }
109
+
110
+ // Call periodically or after storage changes
111
+ await updateStorageUsage(
112
+ 'cus_abc',
113
+ 'snapshot_01K1M4D2K9',
114
+ 5368709120,
115
+ '2026-08-01T10:30:00Z',
116
+ ); // 5GB
117
+ ```
118
+
119
+ ---