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
TenantBillingrecord in KV tracking:replies_remaining,replies_used(lifetime), andreplies_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:
- Tenant selects a credit pack on the pricing page (
/pricing) - Concierge creates a Razorpay order via the API
- The Razorpay checkout widget opens in the browser
- On successful payment, Razorpay sends a webhook to
POST /webhook/razorpay - Concierge verifies the payment signature (HMAC-SHA256) and credits the tenant's balance
- The payment is recorded in the
paymentsD1 table
Required Secrets
RAZORPAY_KEY_ID: Razorpay API key IDRAZORPAY_KEY_SECRET: Razorpay API key secretRAZORPAY_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), andmax_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.
| Concept | Unit (every currency) | Used by |
|---|---|---|
unit_price_milli | milli-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_price | minor units | Reply-email pack price per recurring period. |
verification_amount | minor units | Sign-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_purchasedcounter so they can claim more local-parts without a Razorpay round-trip. Backed byPOST /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
}