信任与审计

每一笔扣费 — 怎么算、怎么查、怎么保证不重复

我们的计费是 post-settlement 模型:先把请求转发到上游,**拿到真实的 usage 数据后再扣积分**。这意味着扣费金额永远等于上游真实消耗对应的 tier,不是估算。下面把每个步骤摊开。

1. 扣费公式

每个模型在 cc_models 表里有一个 context_tiers 配置 —— up_tokens 阈值映射到 credits。BillingHandler 拿到上游返回的 prompt_tokens + completion_tokens 后,用 input_tokens 落档,扣对应 credits:

// internal/service/model_registry.go
func (m ModelConfig) CreditsForTokens(inputTokens int) int {
    for _, tier := range m.ContextTiers {
        if tier.UpTokens == 0 || inputTokens <= tier.UpTokens {
            return tier.Credits
        }
    }
    return m.ContextTiers[len(m.ContextTiers)-1].Credits
}

// Example — claude-sonnet-4-6 with 18,000 input tokens:
//   tier 1: up_tokens=32000  credits=12  ← matches
//   tier 2: up_tokens=200000 credits=36
//   tier 3: up_tokens=0      credits=84  (terminal "anything bigger")
// → 12 credits deducted, regardless of completion length.

完成 tokens 不影响落档 — 这样长回答不会变成意外账单。

2. 一次请求的完整生命周期

  1. 01你的客户端发请求到 api.clawfeeder.ai,header 里带 Authorization: Bearer cf-sk-...
  2. 02JWT/API Key middleware 校验后,BillingHandler 读 request body 拿到 model 字段
  3. 03查 cc_models 注册表确认 model 存在(否则拒 400 unsupported_model)
  4. 04余额 + trial 门禁检查 — 不够 / 不被授权直接 402
  5. 05向真实上游(channel 链路)转发,5xx 自动 fallback 到下一个 channel
  6. 06**上游返回后**,streaming usage scanner 边转发边扫 usage 字段
  7. 07拿到 usage 后调 CreditsForTokens 算出扣多少
  8. 08写一行 cc_credits_ledger(reason=api_use, delta=负数, model, latency_ms, status_code, ref_id=trace_id)
  9. 09扣款 unique ref_id 保证幂等 — 同一 trace_id 重复 settle 不会重复扣

3. 在哪可以核查每一笔

每一次请求都能从 4 个独立维度交叉验证:

  • 响应头里的 X-Request-ID每次请求服务器返一个 UUID 给你,这是 ledger 那一行的 ref_id,也是日志 trace_id。任何时候出问题,把它发给我们就能定位。
  • 响应头里的 X-Clawfeeder-Model实际结算的真实模型 ID。用 model=auto 时,会显示路由到了哪个具体模型。计费就按这个,与 'auto' 无关。
  • Dashboard 用量页/dashboard/usage 显示每笔扣费(模型、tokens、credits、ref_id、状态码)。可以直接查 30 天历史。
  • GET /api/credits/history程序化访问同一份数据,JSON 输出,方便接你的对账脚本。

4. 幂等保证

cc_credits_ledger.ref_id 列有 UNIQUE partial index。一次请求只有一个 trace_id,即使 BillingHandler 因为某种原因被重试(比如部署时 graceful shutdown 中途,或者 streaming SSE 重连),第二次写入 ledger 会被数据库拒绝,扣款不会重复发生。

-- migration 013 (deployed 2026-04-20):
CREATE UNIQUE INDEX cc_credits_ledger_ref_id_idx
  ON cc_credits_ledger(ref_id)
  WHERE ref_id IS NOT NULL;

视频任务异步扣费也是同一套机制 — 预扣 ref_id=video:<task_id>,结算 ref_id=video-settle:<task_id>,退款 ref_id=video-refund:<task_id>。同一 task_id 不会被结算两次。

5. 我们存了什么、没存什么

✓ 我们存

  • · 请求时间、模型、tokens 数量
  • · 扣的积分、ref_id、上游 HTTP 状态码
  • · 上游延迟(latency_ms)
  • · 用户 ID 和 API key 指纹

✗ 我们不存

  • · Prompt 内容
  • · Response 内容
  • · 请求 / 响应 body 任何片段
  • · (Playground 对话除外 — 那是你主动保存的)

这是技术不变量,不是承诺。BillingHandler 的代码里没有任何把 body 写到 DB 的路径 — 它只走 streaming scanner 找 usage 字段,scan 完丢弃。

6. 余额为零会发生什么

Post-settlement 计费意味着扣款发生在请求之后。我们允许一个 -100 积分的软透支底线 — 这样长请求拿到 usage 时不会因为余额刚好为 0 而被吞掉。一旦余额 ≤ -100,新请求立刻返 402 直到充值。

底线 -100 积分 = 约 $0.30 — 实际超的钱永远很小。

7. 积分有效期与续费

随订阅发放的积分(套餐充值、兑换码)与订阅周期同寿命 — 到期时该批未用完的部分会被清零。这是订阅制的常态;关键在于续费的处理方式对你有利。

  • · 提前续费,有效期叠加 — 续费时间从「当前到期日」往后加,不是从今天重算。早续不丢剩余天数。
  • · 未用完的积分自动顺延 — 续费时,你账上还没用完的积分会跟着展期到新的到期日,一分不丢。
  • · 赠送类积分不过期 — 邀请、推荐、运营赠送的积分没有过期时间,永久有效。

例:会员 7-15 到期、还剩 3,200 积分,你在 7-1 续费 30 天 → 有效期叠加到 8-14,那 3,200 积分也顺延到 8-14。

8. 数据一致性自检

每天 03:17 UTC 一个 reconcile job 跑 4 项只读检查:

  • · duplicate_ref_ids扫描 ledger 是否有重复 ref_id
  • · no_usage_trend检查零费用 ledger 行的趋势(可能 usage parse 失败)
  • · tier_distributiontier 分布是否异常偏向某档
  • · balance_drift用户余额 = SUM(delta) 是否对得上

结果写 Redis 30 天保留。任何异常 finding 会写 WARN/ERROR 日志,可由后续运维流程接告警。

想自己看一笔扣费?

登录后到 Dashboard 用量页查最近 30 天的每一行 ledger。

查看用量 →
计费机制与审计 — clawfeeder.ai