C Concierge Documentation
Docs / Operate / Billing
Operate

Billing & credits.

Concierge uses a prepaid reply credit system. Every auto-reply (WhatsApp, Instagram) and relay reply (Discord) deducts one credit from the tenant's balance. When credits reach zero, auto-replies stop silently.

How Credits Work

  • Each tenant has a TenantBilling record in KV tracking: replies_remaining, replies_used (lifetime), and replies_granted (lifetime)
  • Credits are deducted before the reply is sent (optimistic deduction)
  • If the AI generation or message send fails, the credit is automatically restored
  • An empty reply (e.g., AI returns nothing) also triggers a credit restore
  • One AI reply ≡ one credit, regardless of how much chat history is fed to the model. The multi‑turn context (capped by ConversationConfig::max_history_messages) sharpens the answer but doesn’t multiply the bill.
  • Static (canned) responses, prompt‑injection scanning, persona safety classification, and BGE embedding lookups are all free — only the reply‑model call deducts. Handoff turns under the cooldown are still AI‑generated and still deduct one credit each; once the worker switches to silent mode past the cooldown, no further credits are spent.

Getting Credits

There are two ways to get reply credits:

1. Management Grants (Free)

Platform operators can grant free credits to any tenant via the management panel. This is useful for onboarding, promotions, or support situations. Grants are logged to the audit trail.

2. Paid Top-ups (Razorpay)

Tenants can purchase AI-reply credits via a slider in the dashboard. The credit-purchase bounds (default min 1000, max 1,000,000) and the flat per-reply rate are configurable by platform operators.

The unit price for each currency is stored in the pricing_config table (as milli-units, i.e. 1/1000th of a cent or paisa). This allows operators to adjust pricing globally without changing code.

Prices are stored in paise (INR) and cents (USD) internally for the final transaction.

Razorpay Integration

The payment flow works as follows:

  1. Tenant selects a credit pack on the pricing page (/pricing)
  2. Concierge creates a Razorpay order via the API
  3. The Razorpay checkout widget opens in the browser
  4. On successful payment, Razorpay sends a webhook to POST /webhook/razorpay
  5. Concierge verifies the payment signature (HMAC-SHA256) and credits the tenant's balance
  6. The payment is recorded in the payments D1 table

Required Secrets

  • RAZORPAY_KEY_ID: Razorpay API key ID
  • RAZORPAY_KEY_SECRET: Razorpay API key secret
  • RAZORPAY_WEBHOOK_SECRET: Webhook secret for signature verification

Pricing Management

Platform operators set the global per-reply and per-extra-address prices via the management panel at /manage/billing. There is one flat rate per currency. No tiers, no packs.

Prices are stored per-currency, in each currency's own units. There's no exchange rate and no conversion. The schema is split into two tables:

  • pricing_config: singleton (id = 1) holding currency-agnostic settings: email_pack_size (default 5), min_credits (default 1000), and max_credits (default 1,000,000).
  • pricing_amount(concept, currency_code, amount): one row per (concept, currency) cell. Currency codes are ISO 4217 (INR, USD, EUR, JPY, KWD, …). The unit is determined by the concept and is the same for every currency.
ConceptUnit (every currency)Used by
unit_price_millimilli-minor (1/1000 of paise / cent / fils / …)Per-AI-reply rate. Stored fine-grained so sub-minor prices fit (e.g. ₹0.10 = 10000 mp).
address_priceminor unitsReply-email pack price per recurring period.
verification_amountminor unitsSign-up verification charge (auto-refunded).

Currency metadata (symbol, ISO name, minor-unit exponent) comes from the rusty_money crate, so the form labels and tenant-facing money formatting stay correct for any ISO 4217 code.

Adding a currency is a UI-only step: the management form's "Add currency" picker lets the operator choose any ISO code; submitting the form seeds default rows and re-renders the table with the new column. There's no migration to run.

Edits to the pricing form trigger a POST /manage/billing/settings. Each per-cell input has a name like unit_price_milli__USD; the handler walks every known currency × concept and upserts cells whose values changed. The settings POST also writes an update_pricing audit-log entry with the full submitted form.

Subscription enforcement, not yet wired: the reply-email pack is described as recurring monthly, but the current Razorpay integration creates one-time orders. A successful payment grants email_pack_size addresses for the lifetime of the account. A follow-up change should switch this flow to Razorpay Subscriptions, with a webhook handler that revokes pack-granted addresses when the subscription lapses.

All grants and pricing edits are recorded in the audit log.

Per-tenant grants

Operators can also grant credits or extra reply-email addresses to a specific tenant from that tenant's detail page (/manage/tenants/{id}):

  • Grant free replies: adds an expiring batch of credits straight to the tenant's ledger. Backed by POST /manage/tenants/{id}/grant-replies.
  • Grant reply-email addresses: bumps the tenant's email_address_extras_purchased counter so they can claim more local-parts without a Razorpay round-trip. Backed by POST /manage/tenants/{id}/grant-addresses.

Billing State

Each tenant's billing state is stored in KV as a TenantBilling JSON object:

{
  "replies_remaining": 450,
  "replies_used": 50,
  "replies_granted": 500
}