@dodopayments/opencode-plugin 0.2.0
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.
- package/.mcp.json +22 -0
- package/LICENSE +21 -0
- package/README.md +195 -0
- package/opencode-plugin/README.md +42 -0
- package/opencode-plugin/index.js +30 -0
- package/opencode.json +28 -0
- package/package.json +53 -0
- package/skills/best-practices/SKILL.md +315 -0
- package/skills/billing-sdk/SKILL.md +501 -0
- package/skills/checkout-integration/SKILL.md +599 -0
- package/skills/credit-based-billing/SKILL.md +942 -0
- package/skills/license-keys/SKILL.md +591 -0
- package/skills/subscription-integration/SKILL.md +571 -0
- package/skills/usage-based-billing/SKILL.md +680 -0
- package/skills/webhook-integration/SKILL.md +529 -0
|
@@ -0,0 +1,680 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: usage-based-billing
|
|
3
|
+
description: Guide for implementing usage-based billing with Dodo Payments - meters, events, pricing per unit, and metered subscriptions.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Dodo Payments Usage-Based Billing
|
|
7
|
+
|
|
8
|
+
**Reference: [docs.dodopayments.com/features/usage-based-billing](https://docs.dodopayments.com/features/usage-based-billing/introduction)**
|
|
9
|
+
|
|
10
|
+
Charge customers for what they actually use—API calls, storage, AI tokens, or any metric you define.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Overview
|
|
15
|
+
|
|
16
|
+
Usage-based billing is perfect for:
|
|
17
|
+
- **APIs**: Charge per request or operation
|
|
18
|
+
- **AI Services**: Bill per token, generation, or inference
|
|
19
|
+
- **Infrastructure**: Charge for compute, storage, bandwidth
|
|
20
|
+
- **SaaS**: Metered features alongside subscriptions
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Core Concepts
|
|
25
|
+
|
|
26
|
+
### Events
|
|
27
|
+
Usage actions sent from your application:
|
|
28
|
+
```json
|
|
29
|
+
{
|
|
30
|
+
"event_id": "evt_unique_123",
|
|
31
|
+
"customer_id": "cus_abc123",
|
|
32
|
+
"event_name": "api.call",
|
|
33
|
+
"timestamp": "2025-01-21T10:30:00Z",
|
|
34
|
+
"metadata": { "endpoint": "/v1/users", "tokens": 150 }
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Meters
|
|
39
|
+
Aggregate events into billable quantities:
|
|
40
|
+
| Aggregation | Use Case | Example |
|
|
41
|
+
|-------------|----------|---------|
|
|
42
|
+
| **Count** | Total events | API calls, image generations |
|
|
43
|
+
| **Sum** | Add values | Tokens used, bytes transferred |
|
|
44
|
+
| **Max** | Highest value | Peak concurrent users |
|
|
45
|
+
| **Last** | Most recent | Current storage used |
|
|
46
|
+
|
|
47
|
+
### Products with Usage Pricing
|
|
48
|
+
- Price per unit (e.g., $0.001 per API call)
|
|
49
|
+
- Free threshold (e.g., 1,000 free calls)
|
|
50
|
+
- Automatic billing each cycle
|
|
51
|
+
|
|
52
|
+
**Billing Example**: 2,500 calls - 1,000 free = 1,500 × $0.02 = $30.00
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## Quick Start
|
|
57
|
+
|
|
58
|
+
### 1. Create a Meter
|
|
59
|
+
|
|
60
|
+
In Dashboard → Meters → Create Meter:
|
|
61
|
+
|
|
62
|
+
1. **Name**: "API Requests"
|
|
63
|
+
2. **Event Name**: `api.call` (exact match, case-sensitive)
|
|
64
|
+
3. **Aggregation**: Count
|
|
65
|
+
4. **Unit**: "calls"
|
|
66
|
+
|
|
67
|
+
### 2. Create Usage-Based Product
|
|
68
|
+
|
|
69
|
+
In Dashboard → Products → Create Product:
|
|
70
|
+
|
|
71
|
+
1. Select **Usage-Based** type
|
|
72
|
+
2. Connect your meter
|
|
73
|
+
3. Set pricing:
|
|
74
|
+
- **Price Per Unit**: $0.001
|
|
75
|
+
- **Free Threshold**: 1000
|
|
76
|
+
|
|
77
|
+
### 3. Send Events
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
import DodoPayments from 'dodopayments';
|
|
81
|
+
|
|
82
|
+
const client = new DodoPayments({
|
|
83
|
+
bearerToken: process.env.DODO_PAYMENTS_API_KEY,
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
await client.usageEvents.ingest({
|
|
87
|
+
events: [{
|
|
88
|
+
event_id: `api_${Date.now()}_${Math.random()}`,
|
|
89
|
+
customer_id: 'cus_abc123',
|
|
90
|
+
event_name: 'api.call',
|
|
91
|
+
timestamp: new Date().toISOString(),
|
|
92
|
+
metadata: {
|
|
93
|
+
endpoint: '/v1/users',
|
|
94
|
+
method: 'GET',
|
|
95
|
+
}
|
|
96
|
+
}]
|
|
97
|
+
});
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## Implementation Examples
|
|
103
|
+
|
|
104
|
+
### TypeScript/Node.js
|
|
105
|
+
|
|
106
|
+
```typescript
|
|
107
|
+
import DodoPayments from 'dodopayments';
|
|
108
|
+
|
|
109
|
+
const client = new DodoPayments({
|
|
110
|
+
bearerToken: process.env.DODO_PAYMENTS_API_KEY!,
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// Track single event
|
|
114
|
+
async function trackUsage(
|
|
115
|
+
customerId: string,
|
|
116
|
+
eventName: string,
|
|
117
|
+
metadata: Record<string, string>
|
|
118
|
+
) {
|
|
119
|
+
await client.usageEvents.ingest({
|
|
120
|
+
events: [{
|
|
121
|
+
event_id: `${eventName}_${Date.now()}_${crypto.randomUUID()}`,
|
|
122
|
+
customer_id: customerId,
|
|
123
|
+
event_name: eventName,
|
|
124
|
+
timestamp: new Date().toISOString(),
|
|
125
|
+
metadata,
|
|
126
|
+
}]
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Track API call
|
|
131
|
+
await trackUsage('cus_abc123', 'api.call', {
|
|
132
|
+
endpoint: '/v1/generate',
|
|
133
|
+
method: 'POST',
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// Track token usage (for Sum aggregation)
|
|
137
|
+
await trackUsage('cus_abc123', 'token.usage', {
|
|
138
|
+
tokens: '1500',
|
|
139
|
+
model: 'gpt-4',
|
|
140
|
+
});
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Batch Event Ingestion
|
|
144
|
+
|
|
145
|
+
Send multiple events efficiently (max 1000 per request):
|
|
146
|
+
|
|
147
|
+
```typescript
|
|
148
|
+
async function trackBatchUsage(
|
|
149
|
+
events: Array<{
|
|
150
|
+
customerId: string;
|
|
151
|
+
eventName: string;
|
|
152
|
+
metadata: Record<string, string>;
|
|
153
|
+
timestamp?: string;
|
|
154
|
+
}>
|
|
155
|
+
) {
|
|
156
|
+
const formattedEvents = events.map((e, i) => ({
|
|
157
|
+
event_id: `batch_${Date.now()}_${i}_${crypto.randomUUID()}`,
|
|
158
|
+
customer_id: e.customerId,
|
|
159
|
+
event_name: e.eventName,
|
|
160
|
+
timestamp: e.timestamp || new Date().toISOString(),
|
|
161
|
+
metadata: e.metadata,
|
|
162
|
+
}));
|
|
163
|
+
|
|
164
|
+
await client.usageEvents.ingest({ events: formattedEvents });
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Batch track multiple API calls
|
|
168
|
+
await trackBatchUsage([
|
|
169
|
+
{ customerId: 'cus_abc', eventName: 'api.call', metadata: { endpoint: '/v1/users' } },
|
|
170
|
+
{ customerId: 'cus_abc', eventName: 'api.call', metadata: { endpoint: '/v1/orders' } },
|
|
171
|
+
{ customerId: 'cus_xyz', eventName: 'api.call', metadata: { endpoint: '/v1/products' } },
|
|
172
|
+
]);
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### Python
|
|
176
|
+
|
|
177
|
+
```python
|
|
178
|
+
from dodopayments import DodoPayments
|
|
179
|
+
import uuid
|
|
180
|
+
from datetime import datetime
|
|
181
|
+
|
|
182
|
+
client = DodoPayments(bearer_token=os.environ["DODO_PAYMENTS_API_KEY"])
|
|
183
|
+
|
|
184
|
+
def track_usage(customer_id: str, event_name: str, metadata: dict):
|
|
185
|
+
client.usage_events.ingest(events=[{
|
|
186
|
+
"event_id": f"{event_name}_{datetime.now().timestamp()}_{uuid.uuid4()}",
|
|
187
|
+
"customer_id": customer_id,
|
|
188
|
+
"event_name": event_name,
|
|
189
|
+
"timestamp": datetime.now().isoformat(),
|
|
190
|
+
"metadata": metadata
|
|
191
|
+
}])
|
|
192
|
+
|
|
193
|
+
# Track AI token usage
|
|
194
|
+
track_usage("cus_abc123", "ai.tokens", {
|
|
195
|
+
"tokens": "2500",
|
|
196
|
+
"model": "claude-3",
|
|
197
|
+
"operation": "completion"
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
# Track image generation
|
|
201
|
+
track_usage("cus_abc123", "image.generated", {
|
|
202
|
+
"size": "1024x1024",
|
|
203
|
+
"model": "dall-e-3"
|
|
204
|
+
})
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
### Go
|
|
208
|
+
|
|
209
|
+
```go
|
|
210
|
+
package main
|
|
211
|
+
|
|
212
|
+
import (
|
|
213
|
+
"context"
|
|
214
|
+
"fmt"
|
|
215
|
+
"os"
|
|
216
|
+
"time"
|
|
217
|
+
|
|
218
|
+
"github.com/dodopayments/dodopayments-go"
|
|
219
|
+
"github.com/google/uuid"
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
func main() {
|
|
223
|
+
client := dodopayments.NewClient(
|
|
224
|
+
option.WithBearerToken(os.Getenv("DODO_PAYMENTS_API_KEY")),
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
ctx := context.Background()
|
|
228
|
+
|
|
229
|
+
_, err := client.UsageEvents.Ingest(ctx, &dodopayments.UsageEventIngestParams{
|
|
230
|
+
Events: []dodopayments.UsageEvent{{
|
|
231
|
+
EventID: fmt.Sprintf("api_%d_%s", time.Now().Unix(), uuid.New().String()),
|
|
232
|
+
CustomerID: "cus_abc123",
|
|
233
|
+
EventName: "api.call",
|
|
234
|
+
Timestamp: time.Now().Format(time.RFC3339),
|
|
235
|
+
Metadata: map[string]string{
|
|
236
|
+
"endpoint": "/v1/users",
|
|
237
|
+
"method": "GET",
|
|
238
|
+
},
|
|
239
|
+
}},
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
if err != nil {
|
|
243
|
+
panic(err)
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
---
|
|
249
|
+
|
|
250
|
+
## Meter Configuration
|
|
251
|
+
|
|
252
|
+
### Aggregation Types
|
|
253
|
+
|
|
254
|
+
#### Count (API Calls, Requests)
|
|
255
|
+
```
|
|
256
|
+
Meter: API Requests
|
|
257
|
+
Event Name: api.call
|
|
258
|
+
Aggregation: Count
|
|
259
|
+
Unit: calls
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
#### Sum (Tokens, Bytes)
|
|
263
|
+
```
|
|
264
|
+
Meter: Token Usage
|
|
265
|
+
Event Name: token.usage
|
|
266
|
+
Aggregation: Sum
|
|
267
|
+
Over Property: tokens
|
|
268
|
+
Unit: tokens
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
Events must include the property in metadata:
|
|
272
|
+
```typescript
|
|
273
|
+
await client.usageEvents.ingest({
|
|
274
|
+
events: [{
|
|
275
|
+
event_id: 'token_123',
|
|
276
|
+
customer_id: 'cus_abc',
|
|
277
|
+
event_name: 'token.usage',
|
|
278
|
+
metadata: { tokens: '1500' } // This value gets summed
|
|
279
|
+
}]
|
|
280
|
+
});
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
#### Max (Peak Concurrent Users)
|
|
284
|
+
```
|
|
285
|
+
Meter: Peak Users
|
|
286
|
+
Event Name: concurrent.users
|
|
287
|
+
Aggregation: Max
|
|
288
|
+
Over Property: count
|
|
289
|
+
Unit: users
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
#### Last (Current Storage)
|
|
293
|
+
```
|
|
294
|
+
Meter: Storage Used
|
|
295
|
+
Event Name: storage.snapshot
|
|
296
|
+
Aggregation: Last
|
|
297
|
+
Over Property: bytes
|
|
298
|
+
Unit: GB
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
### Event Filtering
|
|
302
|
+
|
|
303
|
+
Filter which events count toward the meter:
|
|
304
|
+
|
|
305
|
+
```
|
|
306
|
+
Filter Logic: AND
|
|
307
|
+
Conditions:
|
|
308
|
+
- Property: tier, Equals: "premium"
|
|
309
|
+
- Property: status, Equals: "success"
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
Only events matching ALL conditions are counted.
|
|
313
|
+
|
|
314
|
+
---
|
|
315
|
+
|
|
316
|
+
## Common Use Cases
|
|
317
|
+
|
|
318
|
+
### AI Token Billing
|
|
319
|
+
|
|
320
|
+
```typescript
|
|
321
|
+
// Meter: AI Tokens (Sum aggregation over "tokens" property)
|
|
322
|
+
|
|
323
|
+
async function trackAIUsage(
|
|
324
|
+
customerId: string,
|
|
325
|
+
promptTokens: number,
|
|
326
|
+
completionTokens: number,
|
|
327
|
+
model: string
|
|
328
|
+
) {
|
|
329
|
+
const totalTokens = promptTokens + completionTokens;
|
|
330
|
+
|
|
331
|
+
await client.usageEvents.ingest({
|
|
332
|
+
events: [{
|
|
333
|
+
event_id: `ai_${Date.now()}_${crypto.randomUUID()}`,
|
|
334
|
+
customer_id: customerId,
|
|
335
|
+
event_name: 'ai.tokens',
|
|
336
|
+
timestamp: new Date().toISOString(),
|
|
337
|
+
metadata: {
|
|
338
|
+
tokens: totalTokens.toString(),
|
|
339
|
+
prompt_tokens: promptTokens.toString(),
|
|
340
|
+
completion_tokens: completionTokens.toString(),
|
|
341
|
+
model,
|
|
342
|
+
}
|
|
343
|
+
}]
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// After AI completion
|
|
348
|
+
await trackAIUsage('cus_abc', 500, 1200, 'gpt-4');
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
### Image Generation
|
|
352
|
+
|
|
353
|
+
```typescript
|
|
354
|
+
// Meter: Images Generated (Count aggregation)
|
|
355
|
+
|
|
356
|
+
async function trackImageGeneration(
|
|
357
|
+
customerId: string,
|
|
358
|
+
imageSize: string,
|
|
359
|
+
model: string
|
|
360
|
+
) {
|
|
361
|
+
await client.usageEvents.ingest({
|
|
362
|
+
events: [{
|
|
363
|
+
event_id: `img_${Date.now()}_${crypto.randomUUID()}`,
|
|
364
|
+
customer_id: customerId,
|
|
365
|
+
event_name: 'image.generated',
|
|
366
|
+
timestamp: new Date().toISOString(),
|
|
367
|
+
metadata: {
|
|
368
|
+
size: imageSize,
|
|
369
|
+
model,
|
|
370
|
+
}
|
|
371
|
+
}]
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
### API Rate Tracking
|
|
377
|
+
|
|
378
|
+
```typescript
|
|
379
|
+
// Middleware for Express/Next.js
|
|
380
|
+
|
|
381
|
+
async function trackAPIUsage(
|
|
382
|
+
req: Request,
|
|
383
|
+
customerId: string
|
|
384
|
+
) {
|
|
385
|
+
await client.usageEvents.ingest({
|
|
386
|
+
events: [{
|
|
387
|
+
event_id: `api_${Date.now()}_${crypto.randomUUID()}`,
|
|
388
|
+
customer_id: customerId,
|
|
389
|
+
event_name: 'api.call',
|
|
390
|
+
timestamp: new Date().toISOString(),
|
|
391
|
+
metadata: {
|
|
392
|
+
endpoint: req.url,
|
|
393
|
+
method: req.method,
|
|
394
|
+
user_agent: req.headers['user-agent'] || 'unknown',
|
|
395
|
+
}
|
|
396
|
+
}]
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
### Storage Billing
|
|
402
|
+
|
|
403
|
+
```typescript
|
|
404
|
+
// Meter: Storage (Last aggregation - snapshot of current usage)
|
|
405
|
+
|
|
406
|
+
async function updateStorageUsage(customerId: string, bytesUsed: number) {
|
|
407
|
+
await client.usageEvents.ingest({
|
|
408
|
+
events: [{
|
|
409
|
+
event_id: `storage_${Date.now()}_${customerId}`,
|
|
410
|
+
customer_id: customerId,
|
|
411
|
+
event_name: 'storage.snapshot',
|
|
412
|
+
timestamp: new Date().toISOString(),
|
|
413
|
+
metadata: {
|
|
414
|
+
bytes: bytesUsed.toString(),
|
|
415
|
+
gb: (bytesUsed / 1024 / 1024 / 1024).toFixed(2),
|
|
416
|
+
}
|
|
417
|
+
}]
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// Call periodically or after storage changes
|
|
422
|
+
await updateStorageUsage('cus_abc', 5368709120); // 5GB
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
---
|
|
426
|
+
|
|
427
|
+
## Next.js Integration
|
|
428
|
+
|
|
429
|
+
### API Route for Usage Tracking
|
|
430
|
+
|
|
431
|
+
```typescript
|
|
432
|
+
// app/api/track-usage/route.ts
|
|
433
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
434
|
+
import DodoPayments from 'dodopayments';
|
|
435
|
+
|
|
436
|
+
const client = new DodoPayments({
|
|
437
|
+
bearerToken: process.env.DODO_PAYMENTS_API_KEY!,
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
export async function POST(req: NextRequest) {
|
|
441
|
+
const { customerId, eventName, metadata } = await req.json();
|
|
442
|
+
|
|
443
|
+
try {
|
|
444
|
+
await client.usageEvents.ingest({
|
|
445
|
+
events: [{
|
|
446
|
+
event_id: `${eventName}_${Date.now()}_${crypto.randomUUID()}`,
|
|
447
|
+
customer_id: customerId,
|
|
448
|
+
event_name: eventName,
|
|
449
|
+
timestamp: new Date().toISOString(),
|
|
450
|
+
metadata,
|
|
451
|
+
}]
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
return NextResponse.json({ success: true });
|
|
455
|
+
} catch (error: any) {
|
|
456
|
+
return NextResponse.json(
|
|
457
|
+
{ error: error.message },
|
|
458
|
+
{ status: 500 }
|
|
459
|
+
);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
### Usage Tracking Hook
|
|
465
|
+
|
|
466
|
+
```typescript
|
|
467
|
+
// hooks/useUsageTracking.ts
|
|
468
|
+
import { useCallback } from 'react';
|
|
469
|
+
|
|
470
|
+
export function useUsageTracking(customerId: string) {
|
|
471
|
+
const trackUsage = useCallback(async (
|
|
472
|
+
eventName: string,
|
|
473
|
+
metadata: Record<string, string>
|
|
474
|
+
) => {
|
|
475
|
+
await fetch('/api/track-usage', {
|
|
476
|
+
method: 'POST',
|
|
477
|
+
headers: { 'Content-Type': 'application/json' },
|
|
478
|
+
body: JSON.stringify({ customerId, eventName, metadata }),
|
|
479
|
+
});
|
|
480
|
+
}, [customerId]);
|
|
481
|
+
|
|
482
|
+
return { trackUsage };
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// Usage in component
|
|
486
|
+
function AIChat() {
|
|
487
|
+
const { trackUsage } = useUsageTracking('cus_abc123');
|
|
488
|
+
|
|
489
|
+
const handleGenerate = async () => {
|
|
490
|
+
const result = await generateAIResponse(prompt);
|
|
491
|
+
|
|
492
|
+
// Track token usage
|
|
493
|
+
await trackUsage('ai.tokens', {
|
|
494
|
+
tokens: result.totalTokens.toString(),
|
|
495
|
+
model: 'gpt-4',
|
|
496
|
+
});
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
```
|
|
500
|
+
|
|
501
|
+
---
|
|
502
|
+
|
|
503
|
+
## Hybrid Billing
|
|
504
|
+
|
|
505
|
+
Combine usage-based with subscriptions:
|
|
506
|
+
|
|
507
|
+
### Subscription + Usage Overage
|
|
508
|
+
|
|
509
|
+
```typescript
|
|
510
|
+
// 1. Customer subscribes to plan with included usage
|
|
511
|
+
// Product: Pro Plan - $49/month + $0.01/call after 10,000 free
|
|
512
|
+
|
|
513
|
+
// 2. Track all usage events
|
|
514
|
+
await trackUsage('cus_abc', 'api.call', { endpoint: '/v1/generate' });
|
|
515
|
+
|
|
516
|
+
// 3. Dodo automatically:
|
|
517
|
+
// - Applies free threshold (10,000 calls)
|
|
518
|
+
// - Charges overage at $0.01/call
|
|
519
|
+
// - Combines with subscription fee
|
|
520
|
+
```
|
|
521
|
+
|
|
522
|
+
### Multiple Meters per Product
|
|
523
|
+
|
|
524
|
+
Attach up to 10 meters to a single product:
|
|
525
|
+
|
|
526
|
+
```
|
|
527
|
+
Product: AI Platform
|
|
528
|
+
├── Meter: API Calls ($0.001/call, 1000 free)
|
|
529
|
+
├── Meter: Token Usage ($0.01/1000 tokens)
|
|
530
|
+
├── Meter: Image Generations ($0.05/image, 10 free)
|
|
531
|
+
└── Meter: Storage ($0.10/GB)
|
|
532
|
+
```
|
|
533
|
+
|
|
534
|
+
### Credit-Based Meter Billing
|
|
535
|
+
|
|
536
|
+
Link meters to credit entitlements so usage events deduct from a customer's credit balance instead of charging per-unit:
|
|
537
|
+
|
|
538
|
+
1. Create a credit entitlement (Dashboard → Products → Credits)
|
|
539
|
+
2. Create a usage-based product with a meter
|
|
540
|
+
3. On the meter, toggle **Bill usage in Credits**
|
|
541
|
+
4. Select the credit entitlement and set **Meter units per credit**
|
|
542
|
+
|
|
543
|
+
```typescript
|
|
544
|
+
// Meter: AI Tokens (Sum aggregation over "tokens")
|
|
545
|
+
// Credit: "AI Credits" with 10,000 credits/cycle
|
|
546
|
+
// Meter units per credit: 1000 (1,000 tokens = 1 credit)
|
|
547
|
+
|
|
548
|
+
// Usage events deduct credits automatically
|
|
549
|
+
await client.usageEvents.ingest({
|
|
550
|
+
events: [{
|
|
551
|
+
event_id: `ai_${Date.now()}_${crypto.randomUUID()}`,
|
|
552
|
+
customer_id: 'cus_abc123',
|
|
553
|
+
event_name: 'ai.tokens',
|
|
554
|
+
timestamp: new Date().toISOString(),
|
|
555
|
+
metadata: { tokens: '1500', model: 'gpt-4' }
|
|
556
|
+
}]
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
// Check remaining credit balance
|
|
560
|
+
const balance = await client.creditEntitlements.balances.get(
|
|
561
|
+
'cent_ai_credits',
|
|
562
|
+
'cus_abc123'
|
|
563
|
+
);
|
|
564
|
+
console.log(`Credits remaining: ${balance.available_balance}`);
|
|
565
|
+
```
|
|
566
|
+
|
|
567
|
+
Credit deduction runs via a background worker every minute using FIFO ordering (oldest grants consumed first). When credits run out:
|
|
568
|
+
- **Overage disabled**: Usage is blocked
|
|
569
|
+
- **Overage enabled**: Usage continues and overage is tracked per your configured behavior (forgive, bill, or carry deficit)
|
|
570
|
+
---
|
|
571
|
+
|
|
572
|
+
## Best Practices
|
|
573
|
+
|
|
574
|
+
### 1. Unique Event IDs
|
|
575
|
+
Always generate unique IDs for idempotency:
|
|
576
|
+
```typescript
|
|
577
|
+
const eventId = `${eventName}_${Date.now()}_${crypto.randomUUID()}`;
|
|
578
|
+
```
|
|
579
|
+
|
|
580
|
+
### 2. Batch Events
|
|
581
|
+
For high-volume, batch events (max 1000/request):
|
|
582
|
+
```typescript
|
|
583
|
+
// Queue events and send in batches
|
|
584
|
+
const eventQueue: Event[] = [];
|
|
585
|
+
|
|
586
|
+
function queueEvent(event: Event) {
|
|
587
|
+
eventQueue.push(event);
|
|
588
|
+
if (eventQueue.length >= 100) {
|
|
589
|
+
flushEvents();
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
async function flushEvents() {
|
|
594
|
+
if (eventQueue.length === 0) return;
|
|
595
|
+
const batch = eventQueue.splice(0, 1000);
|
|
596
|
+
await client.usageEvents.ingest({ events: batch });
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// Flush periodically
|
|
600
|
+
setInterval(flushEvents, 5000);
|
|
601
|
+
```
|
|
602
|
+
|
|
603
|
+
### 3. Handle Failures Gracefully
|
|
604
|
+
Implement retry logic for event ingestion:
|
|
605
|
+
```typescript
|
|
606
|
+
async function trackWithRetry(event: Event, retries = 3) {
|
|
607
|
+
for (let i = 0; i < retries; i++) {
|
|
608
|
+
try {
|
|
609
|
+
await client.usageEvents.ingest({ events: [event] });
|
|
610
|
+
return;
|
|
611
|
+
} catch (error) {
|
|
612
|
+
if (i === retries - 1) throw error;
|
|
613
|
+
await sleep(1000 * Math.pow(2, i)); // Exponential backoff
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
```
|
|
618
|
+
|
|
619
|
+
### 4. Include Relevant Metadata
|
|
620
|
+
Add context for debugging and filtering:
|
|
621
|
+
```typescript
|
|
622
|
+
metadata: {
|
|
623
|
+
endpoint: '/v1/generate',
|
|
624
|
+
method: 'POST',
|
|
625
|
+
model: 'gpt-4',
|
|
626
|
+
user_id: 'internal_user_123',
|
|
627
|
+
request_id: requestId,
|
|
628
|
+
}
|
|
629
|
+
```
|
|
630
|
+
|
|
631
|
+
### 5. Monitor in Dashboard
|
|
632
|
+
Check Meters dashboard for:
|
|
633
|
+
- Event volume and trends
|
|
634
|
+
- Usage per customer
|
|
635
|
+
- Aggregated quantities
|
|
636
|
+
|
|
637
|
+
---
|
|
638
|
+
|
|
639
|
+
## Pricing Examples
|
|
640
|
+
|
|
641
|
+
### API Service
|
|
642
|
+
```
|
|
643
|
+
Meter: API Calls (Count)
|
|
644
|
+
Price: $0.001 per call
|
|
645
|
+
Free Threshold: 1,000 calls/month
|
|
646
|
+
|
|
647
|
+
Customer uses 15,000 calls:
|
|
648
|
+
(15,000 - 1,000) × $0.001 = $14.00
|
|
649
|
+
```
|
|
650
|
+
|
|
651
|
+
### AI Token Service
|
|
652
|
+
```
|
|
653
|
+
Meter: Tokens (Sum over "tokens")
|
|
654
|
+
Price: $0.00001 per token ($0.01 per 1,000)
|
|
655
|
+
Free Threshold: 10,000 tokens
|
|
656
|
+
|
|
657
|
+
Customer uses 50,000 tokens:
|
|
658
|
+
(50,000 - 10,000) × $0.00001 = $0.40
|
|
659
|
+
```
|
|
660
|
+
|
|
661
|
+
### Image Generation
|
|
662
|
+
```
|
|
663
|
+
Meter: Images (Count)
|
|
664
|
+
Price: $0.05 per image
|
|
665
|
+
Free Threshold: 10 images
|
|
666
|
+
|
|
667
|
+
Customer generates 100 images:
|
|
668
|
+
(100 - 10) × $0.05 = $4.50
|
|
669
|
+
```
|
|
670
|
+
|
|
671
|
+
---
|
|
672
|
+
|
|
673
|
+
## Resources
|
|
674
|
+
|
|
675
|
+
- [Usage-Based Billing Guide](https://docs.dodopayments.com/features/usage-based-billing/introduction)
|
|
676
|
+
- [Meters Documentation](https://docs.dodopayments.com/features/usage-based-billing/meters)
|
|
677
|
+
- [Event Ingestion](https://docs.dodopayments.com/features/usage-based-billing/event-ingestion)
|
|
678
|
+
- [AI Chat App Tutorial](https://docs.dodopayments.com/developer-resources/build-an-ai-chat-app-with-usage-based-billing)
|
|
679
|
+
- [Hybrid Billing Models](https://docs.dodopayments.com/features/hybrid-billing)
|
|
680
|
+
- [Credit-Based Billing](https://docs.dodopayments.com/features/credit-based-billing)
|