AutoGPT is the vision of accessible AI for everyone, to use and to build on. Our mission is to provide the tools, so that you can focus on what matters.
fix(backend): default-off the subscription credit grant + dedup stripe events (#13093)
## Why
**Product principle:** a paid subscription buys rate-limit / copilot
multipliers for that tier — it does **not** add AutoGPT
automation-credit balance. Automation credits are a separate top-up
product. A subscriber paying $50/mo should not also see $50 land in
their automation-credit balance.
[#12933](https://github.com/Significant-Gravitas/AutoGPT/pull/12933)
violated that principle: it wired `invoice.payment_succeeded` to grant
AutoGPT automation credits equal to `invoice.amount_paid` on every paid
Stripe subscription invoice. The justification in that PR description
was self-referential — it cited an upgrade-dialog string ("credit to
your next Stripe invoice") that the same author had added a fortnight
earlier (commit `3e9f16185`) for a completely different reason: to
clarify that Stripe applies proration as a credit on the *next Stripe
invoice*, **not** to the AutoGPT balance. No product spec, Linear
ticket, or stakeholder decision backs the grant behaviour.
In production, a single Pro Monthly user who upgraded mid-cycle observed
two unintended automation-credit grants — both violations of the
principle above:
- `$50` credit on the Pro Monthly cycle invoice — subscription invoices
shouldn't grant automation credits at all
- `$460.02` ~19 min later — a prorated upgrade-to-yearly invoice (`$50 ×
12 × 0.766 ≈ $459.6`) with a *distinct* `invoice_id`, so the per-invoice
idempotency key correctly didn't collapse it with the first; same
principle violation, just a different invoice
Both grants are "working as coded" but neither matches the product
model. The cleanest fix is to default the behaviour off and keep the
code path so it can be opted into later if product ever revisits the
model.
Separately, the webhook entry point lacked event-level idempotency:
every downstream handler (`fulfill_checkout`,
`sync_subscription_from_stripe`, `handle_dispute`, `deduct_credits`,
etc.) had to be independently safe against same-`event.id` retries, and
not all of them are.
## What
### 1. Default-off the subscription credit grant
- New `Config.enable_subscription_credit_grant: bool = False` in
`settings.py`.
- `handle_subscription_payment_success` short-circuits when off;
function + webhook wiring stay so the setting can be flipped on per
environment if product ever revisits the subscription model.
### 2. Frontend dialog copy
- Removed the three "matching credits are added to your AutoGPT balance"
/ "first invoice's amount is added to your balance" / "matching credits
land in your balance" promises in `SubscriptionTierSection` — upgrade
dialog, downgrade dialog, and upgrade-success toast. They were a UX
symptom of the same conflation: subscriptions don't add
automation-credit balance, so the copy shouldn't say they do.
### 3. Stripe webhook event-level idempotency
- New `_claim_stripe_event(event_id)` helper in `v1.py`: Redis `SETNX`
with 24h TTL on `stripe_webhook_event:{event.id}`. Replayed events
short-circuit before any handler runs. Falls open on Redis errors so we
don't drop real events.
## How
- `credit.py:handle_subscription_payment_success` adds the config gate
**after** the existing ENTERPRISE / zero-amount / `paid_out_of_band`
skips, so the precondition checks stay deterministic regardless of the
grant toggle. The failure-handler comment about `paid_out_of_band` was
updated to reflect the now-gated grant path.
- `v1.py:_claim_stripe_event` runs after signature verification and the
defensive `data.object` extraction. Empty `event.id` falls open so the
existing malformed-payload branch (200 + warning) still wins.
- **Test coverage**:
- 7 existing `handle_subscription_payment_success_*` tests keep their
grant-path assertions but explicitly opt the config on via a new
`_patch_credit_grant_config(True)` helper
- New `test_handle_subscription_payment_success_skips_when_disabled`
asserts the default-off no-op
- New `test_stripe_webhook_skips_handlers_on_replayed_event` covers the
webhook-level dedup path
## Test plan
- [ ] CI green
- [ ] Manual: confirm dialog copy renders without credit-grant promises
on `dev-builder.agpt.co`
- [ ] Manual: hit the webhook twice with the same `event.id` and confirm
the second invocation logs the dedup short-circuit
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Bently <Github@bentlybro.com>
Co-authored-by: Devendra Reddy Pennabadi <send2devareddy@gmail.com>
Co-authored-by: devareddy05 <187963739+devareddy05@users.noreply.github.com> Z
Zamil Majdy committed
a0c4b072e0de28415a47f8e674b56dbf3a4f0bfc
Parent: dce0cd9
Committed by GitHub <noreply@github.com>
on 5/12/2026, 7:30:42 PM