diff --git a/autogpt_platform/backend/backend/api/features/subscription_routes_test.py b/autogpt_platform/backend/backend/api/features/subscription_routes_test.py
index f9a4d954b1..681d53023f 100644
--- a/autogpt_platform/backend/backend/api/features/subscription_routes_test.py
+++ b/autogpt_platform/backend/backend/api/features/subscription_routes_test.py
@@ -416,6 +416,98 @@ def test_update_subscription_tier_paid_requires_urls(
assert response.status_code == 422
+def test_update_subscription_tier_currency_mismatch_returns_422(
+ client: fastapi.testclient.TestClient,
+ mocker: pytest_mock.MockFixture,
+) -> None:
+ """Stripe rejects a SubscriptionSchedule whose phases mix currencies (e.g.
+ GBP-checkout sub trying to schedule a USD-only target Price). The handler
+ must convert that into a specific 422 instead of the generic 502 so the
+ caller can tell the difference between a currency-config bug and a Stripe
+ outage."""
+ mock_user = Mock()
+ mock_user.subscription_tier = SubscriptionTier.MAX
+
+ async def mock_feature_enabled(*args, **kwargs):
+ return True
+
+ mocker.patch(
+ "backend.api.features.v1.get_user_by_id",
+ new_callable=AsyncMock,
+ return_value=mock_user,
+ )
+ mocker.patch(
+ "backend.api.features.v1.is_feature_enabled",
+ side_effect=mock_feature_enabled,
+ )
+ mocker.patch(
+ "backend.api.features.v1.modify_stripe_subscription_for_tier",
+ side_effect=stripe.InvalidRequestError(
+ "The price specified only supports `usd`. This doesn't match the"
+ " expected currency: `gbp`.",
+ param="phases",
+ ),
+ )
+
+ response = client.post(
+ "/credits/subscription",
+ json={
+ "tier": "PRO",
+ "success_url": f"{TEST_FRONTEND_ORIGIN}/success",
+ "cancel_url": f"{TEST_FRONTEND_ORIGIN}/cancel",
+ },
+ )
+
+ assert response.status_code == 422
+ detail = response.json()["detail"]
+ assert "billing currency" in detail.lower()
+ assert "contact support" in detail.lower()
+
+
+def test_update_subscription_tier_non_currency_invalid_request_returns_502(
+ client: fastapi.testclient.TestClient,
+ mocker: pytest_mock.MockFixture,
+) -> None:
+ """Locks the contract that *only* currency-mismatch InvalidRequestErrors
+ translate to 422 — every other Stripe InvalidRequestError must still
+ surface as the generic 502 so that widening the conditional later is
+ caught by the suite."""
+ mock_user = Mock()
+ mock_user.subscription_tier = SubscriptionTier.MAX
+
+ async def mock_feature_enabled(*args, **kwargs):
+ return True
+
+ mocker.patch(
+ "backend.api.features.v1.get_user_by_id",
+ new_callable=AsyncMock,
+ return_value=mock_user,
+ )
+ mocker.patch(
+ "backend.api.features.v1.is_feature_enabled",
+ side_effect=mock_feature_enabled,
+ )
+ mocker.patch(
+ "backend.api.features.v1.modify_stripe_subscription_for_tier",
+ side_effect=stripe.InvalidRequestError(
+ "No such price: 'price_does_not_exist'",
+ param="items[0][price]",
+ ),
+ )
+
+ response = client.post(
+ "/credits/subscription",
+ json={
+ "tier": "PRO",
+ "success_url": f"{TEST_FRONTEND_ORIGIN}/success",
+ "cancel_url": f"{TEST_FRONTEND_ORIGIN}/cancel",
+ },
+ )
+
+ assert response.status_code == 502
+ assert "billing currency" not in response.json()["detail"].lower()
+
+
def test_update_subscription_tier_creates_checkout(
client: fastapi.testclient.TestClient,
mocker: pytest_mock.MockFixture,
diff --git a/autogpt_platform/backend/backend/api/features/v1.py b/autogpt_platform/backend/backend/api/features/v1.py
index 396b39f39f..1528097e65 100644
--- a/autogpt_platform/backend/backend/api/features/v1.py
+++ b/autogpt_platform/backend/backend/api/features/v1.py
@@ -1003,6 +1003,35 @@ async def update_subscription_tier(
return await get_subscription_status(user_id)
except ValueError as e:
raise HTTPException(status_code=422, detail=str(e))
+ except stripe.InvalidRequestError as e:
+ # Stripe rejects schedule modify when phases mix currencies, e.g. the
+ # active sub was checked out in GBP but the target tier's Price is
+ # USD-only. 502 reads as outage; surface a 422 with a specific message
+ # so the user/admin can see what to fix in Stripe.
+ msg = str(e)
+ if "currency" in msg.lower():
+ logger.warning(
+ "Currency mismatch on tier change for user %s: %s", user_id, msg
+ )
+ raise HTTPException(
+ status_code=422,
+ detail=(
+ "Tier change unavailable for your current billing currency."
+ " Please contact support — the target tier needs to be"
+ " configured for your currency in Stripe before this"
+ " change can go through."
+ ),
+ )
+ logger.exception(
+ "Stripe error modifying subscription for user %s: %s", user_id, e
+ )
+ raise HTTPException(
+ status_code=502,
+ detail=(
+ "Unable to update your subscription right now. "
+ "Please try again or contact support."
+ ),
+ )
except stripe.StripeError as e:
logger.exception(
"Stripe error modifying subscription for user %s: %s", user_id, e
diff --git a/autogpt_platform/frontend/src/app/(platform)/profile/(user)/credits/components/SubscriptionTierSection/__tests__/SubscriptionTierSection.test.tsx b/autogpt_platform/frontend/src/app/(platform)/profile/(user)/credits/components/SubscriptionTierSection/__tests__/SubscriptionTierSection.test.tsx
index fe17dabf47..f2653dbeba 100644
--- a/autogpt_platform/frontend/src/app/(platform)/profile/(user)/credits/components/SubscriptionTierSection/__tests__/SubscriptionTierSection.test.tsx
+++ b/autogpt_platform/frontend/src/app/(platform)/profile/(user)/credits/components/SubscriptionTierSection/__tests__/SubscriptionTierSection.test.tsx
@@ -736,24 +736,19 @@ describe("SubscriptionTierSection", () => {
).toBeDefined();
});
- it("renders BASIC cancellation copy in banner when pending_tier is BASIC", () => {
+ it("renders cancellation copy in banner when pending_tier is NO_TIER", () => {
setupMocks({
subscription: makeSubscription({
tier: "MAX",
- pendingTier: "BASIC",
- // Noon UTC so the local-formatted date lands on the same day
- // regardless of the runner's timezone (midnight UTC drifts to
- // the prior day in any timezone west of UTC).
+ pendingTier: "NO_TIER",
pendingTierEffectiveAt: new Date("2026-05-15T12:00:00Z"),
}),
});
render(