Internal Documentation internal
TalkIDE internal documentation

Authenticated user reads and updates their monthly spending limit (USD). The limit is stored in user_budget.spending_limit_usd (nullable = unlimited). Soft alert at 80 %, hard soft-stop at 100 % (toast + email if notification preferences allow). Requires a valid JWT access token.

  • spending_limit_usd is NUMERIC(10,2) NULL on user_budget. null means unlimited.
  • The spending limit is a soft cap — it does not block AI inference mid-stream. It triggers:
    • 80 % alert: amber toast in FE + email (if notificationBillingEnabled = true in preferences — defaults to false in MVP since notifications are mockup; email stub only).
    • 100 % alert: rose toast in FE + email (if preferences allow). Mara is soft-stopped (next conversation start is blocked with a SPENDING_LIMIT_REACHED error) until the limit is raised or the month resets.
  • user_budget uses a credit model (migration 0011): columns ai_credit_usd (current remaining credit) and ai_credit_initial_usd (initial credit amount). There is no used_cents column. The “current spent” value is derived: currentSpentUsd = ai_credit_initial_usd - ai_credit_usd. FE reads it from the existing GET /api/v1/users/me/budget endpoint (UC-08006); this UC only adds GET/PUT for the spending_limit_usd field.
  • Validation rule: new spending_limit_usd must be > current spent amount in USD (i.e., > ai_credit_initial_usd - ai_credit_usd; cannot set a limit you’ve already exceeded). null is always valid (clears the limit).
  • Alert thresholds are evaluated BE-side on every Anthropic gateway call (UC-08001) after writing to api_usage_ledger and deducting from user_budget.ai_credit_usd. This UC documents the user-facing GET/PUT endpoints only.
  • DB migration: 0024-add-spending-limit-to-user-budget.xml (new file, immutable once applied).
  • Related: UC-08001 — cost tracking. UC-08006 — existing budget view.
sequenceDiagram
    actor User

    %% --- GET spending limit ---
    User->>+FE: opens Profile → Billing & Usage section

    FE->>FE: onMounted — trigger billing data load

    FE->>+BE: GET /api/v1/users/me/billing/spending-limit <br> Authorization: Bearer {accessToken}

    BE->>BE: validate JWT access token
    alt access token invalid or missing
        BE-->>FE: 401 Unauthorized <br> ErrorResponse
    end

    BE->>DB: SELECT spending_limit_usd, ai_credit_usd, ai_credit_initial_usd FROM user_budget WHERE user_id = userId

    BE->>-FE: 200 OK <br> SpendingLimitResponse

    FE->>-User: render spending limit input (value or "Unlimited" if null)

    %% --- PUT spending limit ---
    User->>+FE: changes spending limit input and clicks "Save"

    FE->>FE: validate input (positive number or blank for unlimited)
    alt input invalid
        FE-->>User: show validation error
    end

    FE->>+BE: PUT /api/v1/users/me/billing/spending-limit <br> Authorization: Bearer {accessToken} <br> UpdateSpendingLimitRequest

    BE->>BE: validate JWT access token
    alt access token invalid or missing
        BE-->>FE: 401 Unauthorized <br> ErrorResponse
    end

    BE->>BE: validate request
    alt request is invalid
        BE-->>FE: 400 Bad Request <br> ErrorResponse
    end

    BE->>DB: SELECT ai_credit_usd, ai_credit_initial_usd FROM user_budget WHERE user_id = userId

    BE->>BE: derive currentSpentUsd = ai_credit_initial_usd - ai_credit_usd <br> check new limit > currentSpentUsd (if limit not null)
    alt new limit <= currentSpentUsd
        BE-->>FE: 400 Bad Request <br> ErrorResponse (SPENDING_LIMIT_BELOW_CURRENT_SPEND)
    end

    BE->>DB: UPDATE user_budget SET spending_limit_usd = newLimit WHERE user_id = userId

    BE->>-FE: 200 OK <br> SpendingLimitResponse

    FE->>-User: show "Spending limit updated" toast, update input value

GET Spending Limit

GET /api/v1/users/me/billing/spending-limit (no request body; authentication via JWT Bearer token)

200 OK SpendingLimitResponse (limit set):

{
  "spendingLimitUsd": 100.00,
  "currentSpentUsd": 23.45,
  "alertThreshold80": false,
  "alertThreshold100": false
}

200 OK SpendingLimitResponse (unlimited):

{
  "spendingLimitUsd": null,
  "currentSpentUsd": 23.45,
  "alertThreshold80": null,
  "alertThreshold100": null
}

Field notes:

  • spendingLimitUsdnull = unlimited; otherwise positive decimal.
  • currentSpentUsd — current month spend in USD, derived as ai_credit_initial_usd - ai_credit_usd. Always returned for FE to display progress.
  • alertThreshold80 / alertThreshold100true if the respective threshold has been crossed; null when limit is null (unlimited). Used by FE to pre-fill the alert state on mount (e.g. if BE already sent the alert but FE re-mounted).

401 Unauthorized ErrorResponse:

{
  "status": 401,
  "code": "AUTHENTICATION_FAILED",
  "message": "Access token is missing or invalid"
}

PUT Spending Limit

PUT /api/v1/users/me/billing/spending-limit UpdateSpendingLimitRequest:

{
  "spendingLimitUsd": 150.00
}

To clear the limit (set to unlimited):

{
  "spendingLimitUsd": null
}

200 OK SpendingLimitResponse:

{
  "spendingLimitUsd": 150.00,
  "currentSpentUsd": 23.45,
  "alertThreshold80": false,
  "alertThreshold100": false
}

400 Bad Request (validation — format error) ErrorResponse:

{
  "status": 400,
  "code": "VALIDATION_ERROR",
  "message": "Validation failed",
  "errors": [
    { "field": "spendingLimitUsd", "message": "must be a positive number" }
  ]
}

400 Bad Request (limit below current spend) ErrorResponse:

{
  "status": 400,
  "code": "SPENDING_LIMIT_BELOW_CURRENT_SPEND",
  "message": "Spending limit cannot be lower than your current month spend of $23.45"
}

401 Unauthorized ErrorResponse:

{
  "status": 401,
  "code": "AUTHENTICATION_FAILED",
  "message": "Access token is missing or invalid"
}

Frontend

Validations

FieldConstraintsSizePatternNote
spendingLimitUsdoptional; if provided: positive number^\d+(\.\d{1,2})?$Blank or empty input = null (unlimited). Reject negative and zero values.

UX Guidelines

Spending limit row in BillingSection.vue:

Spending limit      [$___100___] /month      [Save]
                    (blank = unlimited)
  • Input type number, min="0", step="1" (whole dollars only in MVP). Placeholder: “Unlimited”.
  • “Save” button visible only when value changed from loaded value.
  • After successful PUT: toast (green, 3 s): “Spending limit updated.”

Alert banners (rendered in BillingSection.vue above the usage content):

ThresholdConditionDisplay
80 %currentSpentUsd >= spendingLimitUsd * 0.8Amber banner: “You’ve used {pct}% of your ${limit} spending limit this month.”
100 %currentSpentUsd >= spendingLimitUsdRose banner: “You’ve reached your spending limit. New conversations are paused until the limit is raised or the month resets.”

Banners are re-evaluated after each successful PUT (reactively from SpendingLimitResponse).

Email alert (BE-side, stub in MVP):

  • At 80 %: POST /api/internal/notifications/spending-alert (internal; logs only in MVP).
  • At 100 %: same stub; Mara soft-stop triggered separately by gateway check in UC-08001.

Backend

DB Migration

New migration file: 0024-add-spending-limit-to-user-budget.xml

<changeSet id="0024-add-spending-limit-to-user-budget" author="system">
    <addColumn tableName="user_budget">
        <column name="spending_limit_usd" type="NUMERIC(10,2)">
            <constraints nullable="true"/>
        </column>
    </addColumn>
</changeSet>

Validations

FieldConstraintsNote
JWT Authorization headernot_blank, valid signature, not expired401 if missing/invalid
spendingLimitUsd (PUT)optional; if not null: positive (> 0), max 2 decimal places400 VALIDATION_ERROR if negative or zero
Business rulespendingLimitUsd (if not null) must be > (ai_credit_initial_usd - ai_credit_usd)400 SPENDING_LIMIT_BELOW_CURRENT_SPEND

Test Cases

GIVENWHENTHEN
Authenticated user with spending_limit_usd = nullGET /spending-limit is called200 OK; spendingLimitUsd=null, currentSpentUsd reflects (ai_credit_initial_usd - ai_credit_usd), alertThreshold fields=null
Authenticated user with spending_limit_usd = 100.00, ai_credit_initial_usd = 50.00, ai_credit_usd = 26.55 (spent 23.45)GET /spending-limit is called200 OK; spendingLimitUsd=100.00, currentSpentUsd=23.45, alertThreshold80=false, alertThreshold100=false
Authenticated user with spending_limit_usd = 100.00, ai_credit_initial_usd = 100.00, ai_credit_usd = 15.00 (spent 85.00, 85% threshold)GET /spending-limit is called200 OK; alertThreshold80=true, alertThreshold100=false
Authenticated user, current spend $23.45, new limit $150.00PUT /spending-limit is called200 OK; spending_limit_usd updated to 150.00 in DB; SpendingLimitResponse returned
Authenticated user, current spend $23.45, new limit $20.00 (below spend)PUT /spending-limit is called400 SPENDING_LIMIT_BELOW_CURRENT_SPEND returned
spendingLimitUsd = 0PUT /spending-limit is called400 VALIDATION_ERROR (must be positive) returned
spendingLimitUsd = -10PUT /spending-limit is called400 VALIDATION_ERROR (must be positive) returned
spendingLimitUsd = nullPUT /spending-limit is called200 OK; spending_limit_usd set to null (unlimited); alertThreshold fields null in response
No Authorization headerGET /spending-limit or PUT /spending-limit is called401 AUTHENTICATION_FAILED returned
Authenticated user, spending limit reached (used >= limit)next conversation start is attempted (UC-04002)Mara gateway check raises SPENDING_LIMIT_REACHED; conversation cannot start until limit raised or month resets

Was this page helpful?

Thanks for the feedback.