Skip to content

Free Plan: Reply Cap and Monthly Reset

This document describes how the free plan reply limit is stored, enforced, and reset.

To move to the paid plan, merchants use Shopify App Pricing (hosted plan picker), not an in-app subscription charge — see Shopify App Pricing.


ColumnTypeDescription
free_credit_limitintegerMaximum number of AI replies allowed per month for this shop. Set when the store is created from .env FREE_PLAN_REPLIES_CAP (default 50).
free_credits_usedintegerNumber of AI replies used this month. Incremented by 1 each time the shop (on free plan) receives an AI reply. Reset automatically on UTC day 1 each month by the cron job (see below).

  1. Store creation
    When a new shop is created (OAuth install), free_credit_limit is set from process.env.FREE_PLAN_REPLIES_CAP (default 50). free_credits_used is set to 0.
    See app/shopify.server.js (shop create).

  2. Each AI reply (free tier path only)
    After an AI reply is sent, the app increments free_credits_used by 1 only when the request used the free reply cap (not the USD wallet). If the shop is on the wallet path (plan === "paid" or credit_balance > 0 while plan is free), incrementFreeCreditsUsed is skipped.
    See app/lib/limits.server.js (incrementFreeCreditsUsed, shopUsesCreditWallet) and app/lib/rag.server.js.

  3. Monthly reset (automatic cron)
    On UTC calendar day 1 each month a scheduled job GETs the reset endpoint. The route resets free_credits_used to 0 for all plan = 'free' shops in a single updateMany. Paid shops are not affected.

    Production URL (copy for your scheduler):
    https://ai-chat.appifire.com/api/cron/reset-free-plan-replies

    Use your real app origin if it differs (SHOPIFY_APP_URL); path is always /api/cron/reset-free-plan-replies.

    • Cron expression (UTC): 10 0 1 * * (00:10 UTC, 1st of month).
    • No auth header needed. The route has a built-in UTC day-1 guard: if called on any day other than the 1st it returns { skipped: true } and does not write to the database — accidental calls on other days are completely safe.
    • Emergency manual reset: call resetFreeCreditsUsedForFreePlanShops(prisma) from app/lib/limits.server.js or hit the URL above on UTC day 1.

    Ops reference (cron + compliance + analytics tables): Scheduled jobs, privacy & shop analytics.


  • Before generating an AI reply, the app checks the free plan limit in checkReplyLimit (in app/lib/limits.server.js).
  • If free_credits_used >= free_credit_limit, the app throws REPLY_LIMIT_EXCEEDED and does not generate a reply. The API returns a 429 with a message that the monthly reply limit is exceeded and the merchant should upgrade to the paid plan.
  • The Billing page shows “X of Y replies used” for free plan using free_credits_used and free_credit_limit so merchants can see usage and upgrade when needed.

  • FREE_PLAN_REPLIES_CAP
    Used only when a new shop is created. Its value is stored in shops.free_credit_limit for that shop. Default if unset: 50.
    Existing shops keep their current free_credit_limit; change this env only affects new installs.

  • Migration 20260310000000_add_free_credit_limit_and_used adds free_credit_limit (default 50) and free_credits_used (default 0) to shops.
    Existing shops get these defaults; new shops get free_credit_limit from env at create time.

Free plan with a leftover USD wallet (after cancel)

Section titled “Free plan with a leftover USD wallet (after cancel)”

If shops.plan is free but credit_balance > 0 (merchant canceled subscription but had not spent all purchased/included dollars), chat uses the credit wallet path, not the free monthly reply cap, until the balance reaches zero:

  • shopUsesCreditWallet in app/lib/limits.server.js is true when plan === "paid" OR credit_balance > 0.
  • checkReplyLimit skips the 50-reply cap when the wallet path applies.
  • deductCreditsForReply reduces credit_balance even while plan === "free".
  • After balance hits 0, behavior returns to the normal free reply cap (free_credits_used / free_credit_limit).

Merchants cannot buy top-up packs without an active Shopify subscription (POST /app/billing/buy-credits checks activeSubscriptions). Full detail: Credits wallet: one-time & subscription.