Install
$ agentstack add skill-dodopayments-skills-credit-based-billing ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ● Network access Used
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ● Environment & secrets Used
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
Dodo Payments Credit-Based Billing
Reference: docs.dodopayments.com/features/credit-based-billing
Grant customers a balance of credits (API calls, tokens, compute units, or any custom metric) and deduct from that balance as they consume your service.
Overview
Credit-based billing lets you:
- Issue credits with subscriptions, one-time purchases, or via API
- Deduct automatically via usage meters or manually via API
- Configure rollover to carry unused credits forward
- Handle overage when credits run out mid-cycle
- Set expiration rules per credit entitlement
- Track everything via a full audit ledger
Credits work across all product types: subscriptions, one-time purchases, and usage-based billing.
Core Concepts
Credit Types
| Type | Description | Best For | |------|-------------|----------| | Custom Unit | Your own metric (tokens, API calls, compute hours) with configurable precision (0–3 decimals) | API calls, AI tokens, compute hours, messages | | Fiat Credits | Real currency value (USD, EUR, etc.) that depletes as customers use your service | Prepaid balances, promotional credits, compensation |
Credit Lifecycle
- Credits Issued — Granted on purchase (subscription cycle or one-time) or via API
- Credits Consumed — Deducted via meter events or manual API calls
- Credits Expire or Roll Over — At cycle end, unused credits expire or carry forward
- Overage Handling — If balance hits zero, overage is forgiven, billed, or carried as deficit
Grant Sources
| Source | Description | |--------|-------------| | Subscription | Credits issued each billing cycle | | One-Time | Credits issued with a one-time payment | | API | Credits granted manually via API or dashboard | | Rollover | Credits carried over from a previous billing cycle |
Quick Start
1. Create a Credit Entitlement
import DodoPayments from 'dodopayments';
const client = new DodoPayments({
bearerToken: process.env.DODO_PAYMENTS_API_KEY,
});
const credit = await client.creditEntitlements.create({
name: 'API Credits',
credit_type: 'custom_unit',
unit_name: 'API Calls',
precision: 0,
expiry_duration: 30, // days
rollover_enabled: false,
allow_overage: false,
});
2. Attach Credits to a Product
In Dashboard → Products → Create/Edit Product → Entitlements → Attach Credits:
- Select the credit entitlement
- Set credits issued per billing cycle (subscriptions) or total (one-time)
- Configure trial credits, proration, low balance threshold
3. Create Checkout with Credit Product
const session = await client.checkoutSessions.create({
product_cart: [
{
product_id: 'prod_ai_pro_plan', // Product with credits attached
quantity: 1,
}
],
customer: { email: 'customer@example.com' },
return_url: 'https://yourapp.com/success',
});
// Redirect to session.checkout_url
4. Deduct Credits via Usage Events
// Meter linked to credit entitlement deducts automatically
await client.usageEvents.ingest({
events: [{
event_id: `gen_${Date.now()}_${crypto.randomUUID()}`,
customer_id: 'cus_abc123',
event_name: 'ai.generation',
timestamp: new Date().toISOString(),
metadata: { model: 'gpt-4', tokens: '1500' }
}]
});
5. Check Balance
const balance = await client.creditEntitlements.balances.get(
'cent_credit_id',
'cus_abc123'
);
console.log(`Available: ${balance.available_balance}`);
console.log(`Overage: ${balance.overage_balance}`);
API Reference
Credit Entitlement CRUD
| Operation | Method | Endpoint | |-----------|--------|----------| | Create | POST | /credit-entitlements | | List | GET | /credit-entitlements | | Get | GET | /credit-entitlements/{id} | | Update | PATCH | /credit-entitlements/{id} | | Delete | DELETE | /credit-entitlements/{id} | | Undelete | POST | /credit-entitlements/{id}/undelete |
Balance & Ledger Operations
| Operation | Method | Endpoint | |-----------|--------|----------| | List All Balances | GET | /credit-entitlements/{id}/balances | | Get Customer Balance | GET | /credit-entitlements/{id}/balances/{customer_id} | | Create Ledger Entry | POST | /credit-entitlements/{id}/balances/{customer_id}/ledger-entries | | List Customer Ledger | GET | /credit-entitlements/{id}/balances/{customer_id}/ledger | | List Customer Grants | GET | /credit-entitlements/{id}/balances/{customer_id}/grants |
Implementation Examples
TypeScript/Node.js
Create Credit Entitlement
import DodoPayments from 'dodopayments';
const client = new DodoPayments({
bearerToken: process.env.DODO_PAYMENTS_API_KEY!,
});
// Custom unit credit (AI tokens)
const tokenCredit = await client.creditEntitlements.create({
name: 'AI Tokens',
credit_type: 'custom_unit',
unit_name: 'tokens',
precision: 0,
expiry_duration: 30,
rollover_enabled: true,
max_rollover_percentage: 25,
rollover_timeframe: 'month',
max_rollover_count: 3,
allow_overage: true,
overage_limit: 50000,
price_per_unit: 0.001,
overage_behavior: 'bill_overage_at_billing',
});
// Fiat credit (USD balance)
const usdCredit = await client.creditEntitlements.create({
name: 'Platform Credits',
credit_type: 'fiat',
unit_currency: 'USD',
expiry_duration: 90,
rollover_enabled: false,
allow_overage: false,
});
Manual Credit/Debit via Ledger Entry
// Grant credits manually (e.g., promotional bonus)
await client.creditEntitlements.balances.createLedgerEntry(
'cent_credit_id',
'cus_abc123',
{
type: 'credit',
amount: '500',
description: 'Welcome bonus - 500 free API credits',
idempotency_key: `welcome_bonus_${customerId}`,
}
);
// Debit credits manually (e.g., service compensation deduction)
await client.creditEntitlements.balances.createLedgerEntry(
'cent_credit_id',
'cus_abc123',
{
type: 'debit',
amount: '100',
description: 'Manual deduction for premium support',
idempotency_key: `support_deduction_${Date.now()}`,
}
);
Query Customer Balance and Ledger
// Get current balance
const balance = await client.creditEntitlements.balances.get(
'cent_credit_id',
'cus_abc123'
);
console.log(`Balance: ${balance.available_balance}`);
// List all balances for a credit entitlement
const allBalances = await client.creditEntitlements.balances.list(
'cent_credit_id'
);
// Get full transaction history
const ledger = await client.creditEntitlements.balances.listLedger(
'cent_credit_id',
'cus_abc123'
);
for (const entry of ledger.items) {
console.log(`${entry.type}: ${entry.amount} | Balance: ${entry.balance_after}`);
}
// List credit grants
const grants = await client.creditEntitlements.balances.listGrants(
'cent_credit_id',
'cus_abc123'
);
Update Credit Entitlement Settings
await client.creditEntitlements.update('cent_credit_id', {
rollover_enabled: true,
max_rollover_percentage: 50,
allow_overage: true,
overage_limit: 10000,
price_per_unit: 0.002,
overage_behavior: 'bill_overage_at_billing',
});
Python
from dodopayments import DodoPayments
import os
import uuid
from datetime import datetime
client = DodoPayments(bearer_token=os.environ["DODO_PAYMENTS_API_KEY"])
# Create credit entitlement
credit = client.credit_entitlements.create(
name="AI Tokens",
credit_type="custom_unit",
unit_name="tokens",
precision=0,
expiry_duration=30,
rollover_enabled=True,
max_rollover_percentage=25,
allow_overage=True,
overage_limit=50000,
price_per_unit=0.001,
overage_behavior="bill_overage_at_billing",
)
# Grant credits manually
client.credit_entitlements.balances.create_ledger_entry(
credit_entitlement_id="cent_credit_id",
customer_id="cus_abc123",
type="credit",
amount="500",
description="Promotional bonus",
idempotency_key=f"promo_{uuid.uuid4()}",
)
# Check balance
balance = client.credit_entitlements.balances.get(
credit_entitlement_id="cent_credit_id",
customer_id="cus_abc123",
)
print(f"Available: {balance.available_balance}")
# Send usage events that deduct credits
client.usage_events.ingest(events=[{
"event_id": f"api_{datetime.now().timestamp()}_{uuid.uuid4()}",
"customer_id": "cus_abc123",
"event_name": "ai.tokens",
"timestamp": datetime.now().isoformat(),
"metadata": {"tokens": "1500", "model": "gpt-4"}
}])
Go
package main
import (
"context"
"fmt"
"os"
"time"
"github.com/dodopayments/dodopayments-go"
"github.com/google/uuid"
)
func main() {
client := dodopayments.NewClient(
option.WithBearerToken(os.Getenv("DODO_PAYMENTS_API_KEY")),
)
ctx := context.Background()
// Create credit entitlement
credit, err := client.CreditEntitlements.Create(ctx, &dodopayments.CreditEntitlementCreateParams{
Name: "AI Tokens",
CreditType: "custom_unit",
UnitName: "tokens",
Precision: 0,
})
if err != nil {
panic(err)
}
// Get customer balance
balance, err := client.CreditEntitlements.Balances.Get(ctx, credit.ID, "cus_abc123")
if err != nil {
panic(err)
}
fmt.Printf("Balance: %s\n", balance.AvailableBalance)
// Send usage events
_, err = client.UsageEvents.Ingest(ctx, &dodopayments.UsageEventIngestParams{
Events: []dodopayments.UsageEvent{{
EventID: fmt.Sprintf("api_%d_%s", time.Now().Unix(), uuid.New().String()),
CustomerID: "cus_abc123",
EventName: "ai.tokens",
Timestamp: time.Now().Format(time.RFC3339),
Metadata: map[string]string{
"tokens": "1500",
"model": "gpt-4",
},
}},
})
if err != nil {
panic(err)
}
}
Credit Settings
Rollover
Carry unused credits forward to the next billing cycle:
| Setting | Description | |---------|-------------| | Rollover Enabled | Toggle to allow unused credits to carry forward | | Max Rollover Percentage | Limit how much carries over (0–100%) | | Rollover Timeframe | How long rolled-over credits remain valid (day, week, month, year) | | Max Rollover Count | Maximum consecutive rollovers before credits are forfeited |
Example: 200 unused credits at cycle end, 75% rollover → 150 credits carry forward, 50 forfeited.
Overage
Controls what happens when a customer's balance reaches zero mid-cycle:
| Setting | Description | |---------|-------------| | Allow Overage | Let customers continue past zero balance | | Overage Limit | Max credits consumable beyond balance | | Price Per Unit | Cost per additional credit (with currency) | | Overage Behavior | How overage is handled at cycle end |
Overage Behaviors:
| Behavior | Description | |----------|-------------| | Forgive overage at reset | Overage tracked but not billed (default) | | Bill overage at billing | Overage charged on next invoice | | Carry over deficit | Negative balance carries into next cycle | | Carry over deficit (auto-repay) | Deficit auto-repaid from new credits next cycle |
Expiration
| Setting | Description | |---------|-------------| | Credit Expiry | Duration after issuance: 7, 30, 60, 90, custom days, or never | | Trial Credits Expire After Trial | Whether trial-specific credits expire when trial ends |
Webhook Events
Credit-based billing fires these webhook events:
| Event | Description | |-------|-------------| | credit.added | Credits granted to a customer | | credit.deducted | Credits consumed through usage or manual debit | | credit.expired | Unused credits expired | | credit.rolled_over | Credits carried forward to a new grant | | credit.rollover_forfeited | Credits forfeited at max rollover count | | credit.overage_charged | Overage charges applied | | credit.manual_adjustment | Manual credit/debit adjustment made | | credit.balance_low | Balance dropped below configured threshold |
Webhook Handler Example
// app/api/webhooks/dodo/route.ts
import { NextRequest, NextResponse } from 'next/server';
export async function POST(req: NextRequest) {
const event = await req.json();
switch (event.type) {
case 'credit.added':
await handleCreditAdded(event.data);
break;
case 'credit.deducted':
await handleCreditDeducted(event.data);
break;
case 'credit.balance_low':
await handleBalanceLow(event.data);
break;
case 'credit.expired':
await handleCreditExpired(event.data);
break;
case 'credit.overage_charged':
await handleOverageCharged(event.data);
break;
}
return NextResponse.json({ received: true });
}
async function handleCreditAdded(data: any) {
const { customer_id, credit_entitlement_id, amount, balance_after } = data;
// Update internal records
await prisma.creditBalance.upsert({
where: { customerId_creditId: { customerId: customer_id, creditId: credit_entitlement_id } },
create: { customerId: customer_id, creditId: credit_entitlement_id, balance: balance_after },
update: { balance: balance_after },
});
}
async function handleCreditDeducted(data: any) {
const { customer_id, credit_entitlement_id, amount, balance_after } = data;
await prisma.creditBalance.update({
where: { customerId_creditId: { customerId: customer_id, creditId: credit_entitlement_id } },
data: { balance: balance_after },
});
}
async function handleBalanceLow(data: any) {
const {
customer_id,
credit_entitlement_name,
available_balance,
threshold_percent,
} = data;
// Notify the customer
await sendEmail(customer_id, {
subject: `Your ${credit_entitlement_name} balance is running low`,
body: `You have ${available_balance} credits remaining (${threshold_percent}% threshold reached). Consider upgrading your plan or purchasing additional credits.`,
});
}
async function handleCreditExpired(data: any) {
const { customer_id, credit_entitlement_id, amount, balance_after } = data;
await prisma.creditBalance.update({
where: { customerId_creditId: { customerId: customer_id, creditId: credit_entitlement_id } },
data: { balance: balance_after },
});
// Optionally notify customer
await sendCreditExpiryNotification(customer_id, amount);
}
async function handleOverageCharged(data: any) {
const { customer_id, credit_entitlement_id, amount, overage_after } = data;
// Track overage for billing
await prisma.overageRecord.create({
data: {
customerId: customer_id,
creditId: credit_entitlement_id,
amount,
overageBalance: overage_after,
},
});
}
Balance Low Payload
The credit.balance_low event has a distinct payload:
{
"business_id": "bus_xxxxx",
"type": "credit.balance_low",
"timestamp": "2025-08-04T06:15:00.000000Z",
"data": {
"payload_type": "CreditBalanceLow",
"customer_id": "cus_xxxxx",
"subscription_id": "sub_xxxxx",
"credit_entitlement_id": "cent_xxxxx",
"credit_entitlement_name": "API Credits",
"available_balance": "15",
"subscription_credits_amount": "100",
"threshold_percent": 20,
"threshold_amount": "20"
}
}
Usage Billing with Credits
When credits are linked to usage meters, meter events automatically deduct credits. A background worker processes events every minute, converts meter units to credits using your configured rate, and deducts using FIFO ordering (oldest grants first).
How It Works
- Your app sends usage events — Each event includes customer ID, event name, and metadata
- Meters aggregate events — U
…
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: dodopayments
- Source: dodopayments/skills
- License: MIT
- Homepage: https://docs.dodopayments.com/developer-resources/agent-skills
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.