关于 credit-based-billing
credit-based-billing 是用于 Dodo Payments 预付或附带额度计费的技能,面向实现订阅额度、充值包和用量扣减的开发者。它涵盖额度授予、余额生命周期、超额处理,以及可审计的交易账本。
主要功能
- 01定义额度单位与生命周期
- 02配置到期、结转和超额规则
- 03将额度附加到订阅或一次性产品
- 04按计量用量扣减额度
- 05查询余额并审计交易账本
适用场景
- 01为订阅配置周期额度
- 02销售一次性预付额度包
- 03按 API 或计算用量扣减额度
- 04核对客户余额与账本记录
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ dodopayments/ skills --skill credit-based-billing安装后在 Claude Code 中输入/credit-based-billing确认已加载。
附带文件references/ai-token-example.mdreferences/credit-webhooks.md
详细介绍
Dodo Payments 基于积分的计费
使用此技能为令牌、API 调用、计算、存储或以货币计价的价值构建预付费或包含积分的计费,并提供可审计的发放和扣减记录。
何时使用此技能
- 在订阅中包含定期积分额度。
- 销售一次性充值包或预付费用量。
- 扣减 AI、API 或计算消耗的计量积分。
- 发放促销或支持积分,并记录原因。
- 配置过期、结转、低余额提醒或超额用量。
- 根据不可变的账本记录核对客户余额。
选择计费模式
| 需求 | 选择 | 原因 |
|---|---|---|
| 无需跟踪消耗的固定产品 | 纯订阅或一次性付款 | 无需余额或用量处理流程。 |
| 按每个计量单位直接以货币计费 | 纯按用量计费 | 计量器使用 price_per_unit 为用量定价;无需预付积分池。 |
| 包含或预售可消耗额度 | 基于积分的计费 | 发放会创建可供用量消耗的余额。 |
| 先包含积分,再对超出额度的部分收费 | 积分加超额用量 | 先消耗积分池;配置的超额用量机制负责处理不足部分。 |
| 结合基础费用、包含的积分和计量用量 | 链接到积分权益的按用量计费产品 | 计量器会将用量单位转换为积分扣减。 |
基于积分的计费与按用量计费可以互补。使用 usage-based-billing 技能定义并测试计量器;本技能涵盖积分部分以及计量器与积分的关联。
核心概念
积分权益与发放
积分权益是对积分单位及其生命周期的可重复使用定义:名称、单位、精度、过期时间、结转和超额用量。发放是向单个客户发放此类积分。发放来源可以是:
- 订阅,每个计费周期重新发放;
- 一次性产品或附加项;
- 直接通过 API 记入账本的积分;
- 从之前的发放结转而来。
积分控制客户可以消耗多少。单独的 entitlements 资源控制功能访问、文件或许可证交付等履约内容。不要混淆以下 SDK 命名空间:
client.creditEntitlements credit definitions and balances
client.creditEntitlements.balances customer credit balances and ledger
client.entitlements feature/file/license entitlements
client.entitlements.files entitlement files
client.entitlements.grants fulfillment grants
使用 client.customers.listCreditEntitlements(customerID) 查看客户级别的积分权益。不要用 client.customers.listEntitlements(...) 替代;后者读取的是另一套权益系统。
自定义单位积分与法币积分
| 积分模式 | 表示方式 | 示例 |
|---|---|---|
| 自定义单位 | 应用定义的单位 | 令牌、调用次数、计算小时、GB 小时 |
| 法币 | 货币价值 | USD 或 EUR 预付余额 |
自定义单位的精度可配置为 0 到 10;仪表板默认值为 2。创建后无法更改精度,因此请在发放积分前确定精度。对于整次请求等不可分割的单位,请使用 0。只有在小数积分有意义时,才使用小数精度。
法币示例使用货币的最小单位:在文档中的 OpenAI 示例里,5,000 积分代表 USD 50。不要超出文档所述的最小单位模式,推断普遍适用的法币精度规则。所有金额均以货币的最小单位表示(例如,USD 的美分)。
一个产品最多可关联五个积分权益。每个权益都可以有独立的发放和生命周期设置。
余额与账本
余额响应包含 balance、overage、last_transaction_at、标识符和时间戳。将积分金额视为十进制字符串;不要使用二进制浮点数进行运算。
每笔交易都有一条账本记录,其中包含:
balance_before和balance_after;overage_before和overage_after;- 金额和交易类型;
- 来源引用和发放 ID;
- 描述、元数据和时间戳。
此账本是审计记录,并提供交易级别的余额快照。没有单独说明的历史余额快照端点。
初始化 TypeScript SDK
将 API 密钥保留在服务端。Dodo Payments 密钥使用 dodo_test_... 和 dodo_live_... 前缀。
import DodoPayments from 'dodopayments';
const client = new DodoPayments({
bearerToken: process.env['DODO_PAYMENTS_API_KEY'],
environment: 'test_mode',
});
如果省略 environment,SDK 默认使用 live_mode。开发期间请显式使用 test_mode。
创建和配置积分权益
已确认的 TypeScript 方法是 client.creditEntitlements.create(...)。
import DodoPayments from 'dodopayments';
const client = new DodoPayments({
bearerToken: process.env['DODO_PAYMENTS_API_KEY'],
environment: 'test_mode',
});
const tokenCredits = await client.creditEntitlements.create({
name: 'AI Token Credits',
description: 'Credits consumed by model token usage',
unit: 'token credit',
precision: 0,
expires_after_days: 30,
rollover_enabled: true,
rollover_percentage: 25,
rollover_timeframe_count: 1,
rollover_timeframe_interval: 'Month',
max_rollover_count: 2,
overage_enabled: true,
overage_behavior: 'invoice_at_billing',
overage_limit: 100000,
price_per_unit: '1',
currency: 'USD',
});
console.log(tokenCredits.id);
price_per_unit 是按配置货币计价的十进制字符串;货币金额使用该货币的最小单位。本例中,对于 USD,'1' 表示每个超额积分为 1 美分。
已确认的创建选项
| 字段 | 必填 | 含义和限制 |
|---|---|---|
name | 是 | 积分权益的显示名称。 |
unit | 是 | 单位标签,例如 API Calls、Tokens 或 Credits。 |
precision | 是 | 小数位数,范围为 0–10;创建后不可更改。 |
description | 否 | 可选描述。 |
expires_after_days | 否 | 发放后到期前的天数,可为空。 |
rollover_enabled | 是 | 启用或禁用结转。 |
rollover_percentage | 否 | 结转比例,范围为 0–100。 |
rollover_timeframe_count | 否 | 结转有效期的间隔数。 |
rollover_timeframe_interval | 否 | Day、Week、Month 或 Year。 |
max_rollover_count | 否 | 失效前允许连续结转的最大次数。 |
overage_enabled | 是 | 积分耗尽后是否允许处理负余额。 |
overage_behavior | 否 | 周期结束时的行为,取值必须来自下方的精确枚举。 |
overage_limit | 否 | 允许的最大超额单位数。 |
price_per_unit | 条件必填 | 十进制字符串;启用超额时必填。 |
currency | 条件必填 | 存在价格时必填。货币金额使用货币的最小单位。 |
配置结转
结转会将符合条件的未使用积分带入新的授予额度。配置方法:
- 设置
rollover_enabled: true。 - 将
rollover_percentage设为0到100之间的结转上限。 - 同时设置
rollover_timeframe_count和rollover_timeframe_interval;不能只提供其中一个。 - 如果积分只能连续结转有限次数,请设置
max_rollover_count。
结转在到期前应用;只有剩余部分会到期。
配置到期
API 使用可为空的 expires_after_days 表示到期时间。控制面板提供 7、30、60、90 天、自定义天数或永不过期选项;默认值为 30 天,自定义到期时间至少为 1 天。仅在当前 API 表单允许的情况下,使用 null 表示不通过 API 配置到期时间。
配置超额
精确的 overage_behavior 值 | 计费周期结束时的结果 |
|---|---|
forgive_at_reset | 不收费,也不保留负余额。这是默认值。 |
invoice_at_billing | 在计费时收取超额费用,然后重置负余额。 |
carry_deficit | 保留负余额。 |
carry_deficit_auto_repay | 保留负余额,并从新发放的积分中偿还。 |
当 overage_enabled 为 true 时,必须设置 price_per_unit;存在价格时,必须设置 currency。使用 overage_limit 限制允许的超额单位数。
禁用超额后,Dodo 会在余额降至零时停止扣减。计量处理是异步的,因此这不能用作应用请求的同步授权。若请求必须立即被拒绝,请在应用端实现强制校验。
将积分关联到产品和订阅
在创建或编辑产品的流程中,将积分作为积分权益关联到产品。不要使用 client.entitlements.grants 来关联或发放积分余额。
订阅产品
在 Products → Create Product 中,或在现有产品中:
- 选择订阅定价。
- 打开 Entitlements & Credits,并关联现有的额度权益。
- 设置每个计费周期发放的额度。
- 可选设置余额不足通知额度、试用额度、试用结束后未使用的试用额度是否过期,以及计划变更时的额度按比例调整规则。
- 保留权益的默认结转、超额使用和过期规则,或针对该产品自定义这些规则。
客户会在每个计费周期收到新的额度发放。
一次性付费产品或附加项
将额度权益关联到单次付款产品或附加项,并配置一次性额度。购买后会发放一笔额度。这是预付额度包、充值和促销套餐所支持的形式。
按用量计费的产品
将额度权益关联到与计量器相同的按用量计费产品。已确认的计量器关联字段如下:
| 字段 | 用途 |
|---|---|
meter_id | 用于汇总用量的计量器。 |
free_threshold | 计费前免除计费的用量阈值——仅适用于计量器按金额计费的情况。对于按额度计费的计量器,此字段会被忽略:每个单位都会从额度余额中扣除。 |
credit_entitlement_id | 此计量器消耗的额度池。 |
meter_units_per_credit | 扣除 1 个额度所需的用量单位数;设置 credit_entitlement_id 时必填。 |
price_per_unit | 使用纯按用量定价时的直接单位价格;为十进制字符串。 |
产品关联请求的结构可能会变化。请在控制面板中配置,或从创建产品 API复制当前架构;不要自行编造 entitlements、credits 或 grants 请求负载。
为已关联的产品发起结账
购买产品会为客户创建额度发放;结账不接受临时指定的额度数量。
const session = await client.checkoutSessions.create({
product_cart: [
{
product_id: 'pdt_ai_pro_plan',
quantity: 1,
},
],
customer: { email: '[email protected]' },
return_url: 'https://app.example.com/billing/success',
});
console.log(session);
请使用 checkoutSessions.create,而不要使用已弃用的 payments.create 或 subscriptions.create 方法。
查找客户的额度权益
当应用需要查找与客户关联的所有额度权益时,请使用客户资源:
const customerCredits = await client.customers.listCreditEntitlements(
'cus_TV52uJWWXt2yIoBBxpjaa',
);
console.log(customerCredits);
如果已知额度权益 ID,且需要精确余额,请使用下方的余额资源。
查询客户余额
已确认的方法会首先接收客户 ID,并在选项对象中接收额度权益 ID:
const customerCreditBalance = await client.creditEntitlements.balances.retrieve(
'cus_TV52uJWWXt2yIoBBxpjaa',
{ credit_entitlement_id: 'cde_ztxm5XJsKxWucRWA3rjdM' },
);
console.log({
balance: customerCreditBalance.balance,
overage: customerCreditBalance.overage,
lastTransactionAt: customerCreditBalance.last_transaction_at,
});
balance 和 overage 是以权益额度单位表示的十进制字符串。除非权益本身代表法币价值,否则它们不是货币金额。
手动发放额度
手动发放额度适用于促销、迁移、以额度形式处理的退款,以及客服补偿。请使用根据业务操作生成的稳定幂等键,确保重试不会重复发放。
async function grantSupportCredits(
customerId: string,
creditEntitlementId: string,
supportCaseId: string,
): Promise<string> {
const entry = await client.creditEntitlements.balances.createLedgerEntry(
customerId,
{
credit_entitlement_id: creditEntitlementId,
amount: '500',
entry_type: 'credit',
expires_at: '2027-01-31T23:59:59Z',
idempotency_key: `support-credit:${supportCaseId}`,
reason: `Support gesture for case ${supportCaseId}`,
metadata: { support_case_id: supportCaseId },
},
);
return entry.id;
}
amount 是由 NUMERIC(38,28) 存储的十进制字符串;其整数部分必须小于 10^10。expires_at 适用于额度。额度数量必须符合该权益配置的精度。
如果相同的 idempotency_key 已存在,API 可能会返回 409。请将其视为需要核对原始操作的信号,而不是生成新键并重试发放的许可。
手动扣除额度
使用相同的方法,并设置 entry_type: 'debit'。扣款会优先消耗最早过期的额度;余额不足时可能返回 400。
async function deductJobCredits(
customerId: string,
creditEntitlementId: string,
jobId: string,
amount: string,
): Promise<{ balanceAfter: string; overageAfter: string }> {
const entry = await client.creditEntitlements.balances.createLedgerEntry(
customerId,
{
credit_entitlement_id: creditEntitlementId,
amount,
entry_type: 'debit',
idempotency_key: `job-debit:${jobId}`,
reason: `Credit deduction for job ${jobId}`,
metadata: { job_id: jobId },
},
);
return {
balanceAfter: entry.balance_after,
overageAfter: entry.overage_after,
};
}
如果某项用量也关联了自动计量器到额度的扣除功能,请勿手动扣除该用量;否则同一笔消耗会被扣除两次。
查看审计记录的账本
SDK 会返回可自动分页的可迭代对象:
for await (const entry of client.creditEntitlements.balances.listLedger(
'cus_TV52uJWWXt2yIoBBxpjaa',
{ credit_entitlement_id: 'cde_ztxm5XJsKxWucRWA3rjdM' },
)) {
console.log({
id: entry.id,
amount: entry.amount,
balanceBefore: entry.balance_before,
balanceAfter: entry.balance_after,
overageBefore: entry.overage_before,
overageAfter: entry.overage_after,
createdAt: entry.created_at,
});
}
请将 Dodo 账本条目 ID 与内部订单、客服工单或作业 ID 一同保存。对账时应比较账本条目和来源引用,而不是根据缓存的余额覆盖历史记录。
配置自动按用量将额度扣减为积分
- 为按用量计费的产品关联积分权益。
- 将计量器添加到同一产品。
- 为该计量器启用 Bill usage in Credits。
- 设置
credit_entitlement_id和meter_units_per_credit。 - 导入事件,其区分大小写的
event_name与计量器匹配。
后台工作进程大约每分钟处理一次新增用量,按计量器进行汇总,使用 meter_units_per_credit 转换计量器单位,并从未过期的授予额度中扣减,优先消耗最早到期的授予额度。按积分计费的计量器没有免费阈值。多个计量器可以按不同转换率消耗同一个共享积分池。
此延迟不容忽视:用量事件被接受,并不意味着随后立即读取的余额已包含相应扣减。
使用 usage-based-billing skill 创建计量器,选择 count、sum、max 或 last,定义其筛选条件,并验证事件批处理和元数据。
AI 令牌端到端示例
完整指南:references/ai-token-example.md。
处理积分 Webhook
完整指南:references/credit-webhooks.md。
涵盖:
- 余额不足的载荷
- 余额不足时的通知流程
安全地执行积分校验
尽力而为的余额检查
对于精度为 0 的权益,服务端可以使用安全的整数解析进行粗略的预检查:
async function hasAtLeastWholeCredits(
customerId: string,
creditEntitlementId: string,
required: bigint,
): Promise<boolean> {
const response = await client.creditEntitlements.balances.retrieve(
customerId,
{ credit_entitlement_id: creditEntitlementId },
);
return BigInt(response.balance) >= required;
}
仅将此方法用于精度为 0 的情况。对于小数积分,请使用任意精度的十进制实现,以保留权益配置的精度。
此读取操作不会预留积分。两个并发请求可能都会看到余额充足,而自动计量器扣减可能尚未反映在余额中。
严格的请求校验
选择以下一种模式:
- 可预测的固定成本: 在提供服务前创建一笔幂等的手动扣款。将余额不足的
400视为拒绝。如果扣款后工作失败,则使用关联原因,另行发起幂等的补偿性积分返还。不要同时对同一用量使用计量器。 - AI 令牌等可变成本: 在你自己的数据存储中原子地预留估算容量,限制每位客户的并发数,之后导入实际用量,并使用 Dodo Webhook 和账本条目核对本地预留。
- 超额用量产品: 根据产品的超额用量策略和你自己的风险限额进行授权,而不只是检查
balance > 0。
授权失败时,返回应用层的需付款/需积分响应,并提供充值或升级路径。不要向客户端暴露 Dodo API 错误或密钥。
避免竞态条件
- 先检查再操作: 远程余额读取后再执行工作,并非原子操作。
- 事件发生后立即读取: 计量器工作进程可能尚未处理该事件。
- 并行工作进程: 没有本地预留或原子扣款时,多个任务可能消耗同一笔表面上可用的余额。
- 使用新 ID 重试: 超时后更改
event_id或idempotency_key可能导致用量或调整重复。 - 将缓存作为权威数据: 通过 Webhook 维护的余额适合展示,不适合严格授权。
- 混用扣减路径: 自动计量器扣减加上手动扣款会导致重复计费。
常见错误
- 混淆两种资源。
creditEntitlements和entitlements并不相同。信用额度余额不在client.entitlements.grants下。 - 调用臆造的余额方法。 应使用
balances.retrieve(customerId, { credit_entitlement_id }),而不是balances.get(creditId, customerId)。 - 账本参数顺序错误。
createLedgerEntry的第一个参数是customerId;credit_entitlement_id应放在请求对象中。 - 使用
type或description进行调整。 已确认的字段是entry_type和reason。 - 补助查询参数顺序错误。
creditEntitlements.balances.listGrants(customerID, params)确实存在,第一个参数是客户 ID,权益应放在params对象中。它返回分页结果,因此应读取.items,而不是将结果当作数组。若要获取客户级摘要,请使用customers.listCreditEntitlements(customerID)。 - 将
available_balance当作余额 API 字段。 已确认的余额响应使用balance和overage;available_balance属于专用的低余额 webhook 负载。 - 随意选择精度。 自定义单位支持
0–10的精度,创建后无法更改。 - 使用 JavaScript 浮点数表示信用额度。 API 金额是十进制字符串。请保留为字符串或使用十进制运算。
- 超出调整上限。 账本金额映射到
NUMERIC(38,28),整数部分必须小于10^10。 - 错误描述货币单位。 货币金额使用最小货币单位。不要把分标为美元。
- 使用不稳定的幂等键。 每次重试都使用时间戳或随机键会破坏去重效果。应根据不可变的业务操作生成幂等键。
- 盲目重试余额不足的扣款。 手动扣款在余额不足时可能返回
400;应引导客户充值或按你的超额使用策略处理。 - 以为事件摄取会同步扣款。 用量计量到信用额度的处理大约每分钟进行一次。
- 将 Dodo 的余额用作逐请求锁。 文档明确警告,不要依据异步报告的计量器余额进行严格授权。
- 计量器名称不匹配。 用量
event_name的匹配区分大小写。 - 在同一批次中发送重复 ID。 同一摄取请求中的重复
event_id会导致整个请求被拒绝。重试时,之前已摄取的 ID 会被忽略。 - 臆造去重窗口。 当前文档要求事件 ID 唯一,但没有公布保留时长。
- 转换关系弄反。
meter_units_per_credit表示获得一个信用额度所需的用量单位数,并且设置credit_entitlement_id时必须提供该字段。 - 期望按信用额度计费的计量器存在免费额度。
free_threshold仅适用于按货币计费的计量器。启用 按信用额度计费用量 后,每个单位都会扣减信用额度;应改用授予的信用额度来表示包含的用量。 - 只提供一个周期字段。
rollover_timeframe_count和rollover_timeframe_interval必须同时提供。 - 周期枚举值大小写错误。 已确认的值为
Day、Week、Month和Year。 - 以为先过期再结转。 系统会先结转,只有剩余部分才会过期。
- 臆造超额使用枚举值。 只能使用
forgive_at_reset、invoice_at_billing、carry_deficit或carry_deficit_auto_repay。 - 遗漏超额使用定价依赖项。 启用超额使用时必须设置
price_per_unit;设置价格时必须提供currency。 - 附加过多信用额度。 当前产品上限为五个信用额度权益。
- 以为结账可以授予任意信用额度。 信用额度必须已附加到购买的产品或附加项上。
- 只处理常见 webhook。 除 added、deducted 和 low-balance 事件外,还应包含
credit.overage_reset和credit.rollover_forfeited。 - 解析未经验证的 webhook。 按
webhook-integration中的说明,使用client.webhooks.unwrap()验证原始请求体;切勿自行实现 HMAC,也不要信任重新序列化的 JSON。 - 臆造历史快照 API。 交易级历史记录应使用账本中的
balance_before和balance_after。 - 使用已弃用的购买方法。 新的结账流程应使用
client.checkoutSessions.create(...),而不是payments.create(...)或subscriptions.create(...)。 - 使用错误的主机地址或密钥格式。 有效主机地址为
https://test.dodopayments.com和https://live.dodopayments.com;密钥以dodo_test_或dodo_live_开头。