FlareStarter 文档
功能与集成

Credits 用量计费

余额 + append-only 流水、幂等入账、防超扣的并发扣减,以及 Stripe 充值包。

Credits 是模板的用量计费基座:org 级余额 + append-only 流水,上面已经接了三个 消费方——Stripe 充值包、座位订阅的周期赠额、AI 对话按条扣费。 你自己的计费点(一次导出、一次渲染、一次 API 调用)照同样姿势挂上来即可。

不变量:钱只走一条路

余额只通过 credits/credits.server.ts 的两个函数移动:

  • grantCredits(db, orgId, amount, reason, ref, now) —— 入账(充值/赠额/调整)。
  • spendCredits(db, orgId, amount, ref, now) —— 扣减。

任何绕过它们直接 UPDATE 余额的代码都是 bug。两个函数各写一行 credit_ledger 流水,delta 带符号、reason 归类(topup / grant / spend / adjust),并记下 balanceAfter——事后审计时每一行都能独立对账,不用从头重放。

幂等:ref 是重放保险

每笔操作可带一个 ref(Stripe session id、请求 id……)。同一 ref 第二次到来时 返回 duplicate、余额不动。这让所有上游天然可重试:

  • Stripe webhook 重放(它保证 at-least-once)→ 充值不会入两次账;
  • 座位订阅每个周期的赠额以 orgsub:<invoice id> 为 ref → 续费事件重放无害;
  • AI 每条消息以 ai:<uuid> 为 ref,失败退款用 <ref>:refund → 退一次就只退一次。

给自己的计费点选 ref 的原则:取上游天然唯一的 id(订单号、消息 id、任务 id), 不要自己造随机数再存映射——ref 本身就是映射。

并发:条件更新防超扣

扣减不做「读余额 → 判断 → 写回」——两个并发请求会同时读到足额然后双双扣成负数。 spendCredits 用一条条件 UPDATE:

UPDATE credit_balance SET balance = balance - ?
WHERE organization_id = ? AND balance >= ?

余额不足时 0 行命中,返回 insufficient;D1 单写者模型下这就是原子的, 任何并发交错都扣不成负数。这也是为什么硬性上限放 D1 而近似限流放 KV (见 API Keys 与限流 的取舍说明)。

充值包:inline price_data,免预建 Price

/app/credits 的充值档位在 credits.config.ts 里就是一个数组(默认 1k/$5、 5k/$20、20k/$60),Checkout 用 inline price_data 现场报价——不需要在 Stripe 后台预建 Price 对象,改档位改代码就行。购买要求 org admin,页面对全体成员可见。

到账走 webhook:metadata.type === 'credits-topup' 的 checkout.session.completed / async_payment_succeeded(延迟到账的 ACH/SEPA 也覆盖,payment_status !== 'paid' 时不预授),以 session id 为 ref 幂等入账。分支顺序等 webhook 细节见 计费与订阅。

周期赠额

座位订阅激活和每次续费自动 grant 一笔周期额度(TEAM_CYCLE_CREDITS,默认 1000,在 orgbilling.config.ts),ref 取 invoice id。订阅页把这写进了卖点文案, 改数额时记得同步。

本地开发

没配 STRIPE_SECRET_KEY 时充值入口优雅降级隐藏,余额与流水照常可看。想给本地 org 塞点测试余额,直接在代码里(或一次性脚本)调 grantCredits,别手写 SQL—— 流水与余额必须一起动。

On this page