Files
HiveGrid-Wireframes/hivegrid-cloud/docs/ops/stripe-meters-migration.md
T

5.9 KiB

Stripe Meters API — Operator Runbook

Audience: operators wiring usage-based billing after the Stripe SDK 13 → 19 migration (B-001, Task #28).

Why: Stripe 19 removed the legacy Usage-Records API (subscriptionItems.createUsageRecord / subscriptionItems.listUsageRecordSummaries) and Recurring.aggregate_usage in favor of the Billing-Meters API (billing.meterEvents.create / billing.meters.listEventSummaries). The boilerplate worker (ai-cost-stripe-report) still drives overage reporting from AICostEvent rows; only the Stripe wire format changed. Operators must configure a Billing-Meter once and migrate existing subscriptions to the new meter-backed price.


Step 1 — Create the Billing-Meter

In the Stripe Dashboard (or via the API):

  1. Billing → Meters → Create meter.
  2. Configure:
    • Display name: AI overage cents
    • Event name: ai-overage (the boilerplate hardcodes this id; change it only if you also override aiOverageStripeMeterId / aiOverageMeterEventName per plan).
    • Aggregation: Sum
    • Customer mapping: map the event payload key stripe_customer_id to the Stripe customer id (default — leave as-is).
    • Value settings: event payload key value, type numeric. The reporter submits the rounded overage block-count in cents.
  3. Copy the resulting Meter id (mtr_…). You will store this in Tenant.aiQuotaOverrideJson.aiOverageStripeMeterId (per-tenant) or in the plan default (per-plan, future admin UI).

CLI alternative:

stripe billing meters create \
  --display-name="AI overage cents" \
  --event-name="ai-overage" \
  --default-aggregation[formula]="sum" \
  --customer-mapping[event_payload_key]="stripe_customer_id" \
  --customer-mapping[type]="by_id" \
  --value-settings[event_payload_key]="value"

Step 2 — Create the metered product + price

  1. Products → Create product → "AI Overage".
  2. Add price:
    • Pricing model: Usage-based
    • Meter: select the meter created in Step 1 (mtr_…).
    • Per-unit cost: the per-cent charge (typically 0.01 USD because the meter aggregates cents).
    • Billing period: Monthly (match the tenant's plan).
  3. Copy the resulting Price id (price_…).

Step 3 — Migrate active subscriptions

For every active subscription that had a metered usage-record line in SDK 13.x:

  1. Add a new subscription item referencing the new price from Step 2. Stripe API:

    stripe subscription_items create \
      --subscription="sub_..." \
      --price="price_..." \
      --quantity=0
    
  2. Remove the old subscription item that referenced the pre-migration metered price (or archive its price so no new subscriptions can attach it).

  3. Confirm: stripe subscriptions retrieve sub_... --expand=items.data.price shows the new price and the meter id in price.recurring.meter.

The boilerplate's overage configuration uses aiOverageStripeMeterId as the sole identifier (the legacy aiOverageStripeSubscriptionItemId field was retired with the Stripe-19 cleanup). Existing tenant JSON overrides that still carry the legacy key are silently ignored by the parser.


Step 4 — Wire the boilerplate

  1. Per-tenant override (admin route /admin/settings/pricing/ai-overage):

    {
      "aiOverageStripeMeterId": "mtr_test_abc...",
      "aiMarkupMultiplier": 2.0,
      "aiOverageBlockCents": 10
    }
    
  2. Per-plan default — edit packages/entitlements/src/ai-overage.ts standardAIOverageDefaults and bake the meter id into the plan blueprint (operator-friendly admin UI is a future task).

  3. Worker bootstrap — ensure the Stripe-19 client is constructed with apiVersion: "2025-10-29.clover" and the worker injects a StripeMeterEventSubmitter whose submit() calls

    stripe.billing.meterEvents.create(
      {
        event_name: "ai-overage",
        payload: {
          stripe_customer_id: tenant.stripeCustomerId,
          value: String(roundedBlocks),
        },
        identifier: idempotencyKey,
        timestamp,
      },
      { idempotencyKey },
    );
    

    See apps/worker/src/jobs/ai-cost-stripe-report.ts for the pure-by-deps interface (StripeMeterEventSubmitter).


Step 5 — Verify with a test event

  1. Toggle AI_OVERAGE_SUBMISSION_ENABLED=true.

  2. Manually emit a test event:

    stripe billing meter_events create \
      --event-name="ai-overage" \
      --payload[stripe_customer_id]="cus_test123" \
      --payload[value]="100" \
      --identifier="manual-test-$(date +%s)"
    
  3. Confirm aggregation:

    stripe billing meters list_event_summaries mtr_... \
      --customer="cus_test123" \
      --start-time=$(date -v-1d +%s) \
      --end-time=$(date +%s)
    

    You should see a non-zero aggregated_value for the meter event you just submitted.

  4. End-to-end: run pnpm --filter @toolctrl/worker test ai-cost-stripe-report to confirm the worker keeps passing its 13 unit tests against the new wire.


Rollback

The Meters API is not trivially reversible — once events are submitted, the legacy Usage-Records API cannot ingest them. The safe rollback path is to leave already-submitted events in place and revert the SDK to 13.x via Git revert (commit 4b2350f0c5a098d3c2dd0778f087cac5910ec2f9 and its follow-ups). The operator-side meter + price can stay in place — Stripe ignores them when no subscription item references them.


Open items

  • Admin UI per-plan meter-id editor (currently only per-tenant via aiQuotaOverrideJson).
  • Auto-creation of the meter on first operator-bootstrap (currently manual via Dashboard / CLI).
  • CLI helper toolctrl-cli ai-cost stripe-init to scaffold the meter + product + price idempotently — listed in docs/improvement-plan/05-business-features/ai-cost-phase-3.md follow-ups.