Pricing — credit packs (pure consumption)
Decided by Salil, 2026-06-11; credit-pack model + financial model 2026-06-23. Source of truth for the billing build (#12) and the partial-processing engine (#23).
The model
Pure consumption, like the AI tools buyers already know: buy credits, burn them, no hidden charges. 1 credit = ₹5 = 1 page (CSV: 40 rows = 1 page). All product features are available to every account regardless of pack size — we never gate the decision engine, API, retention, etc. behind a tier. That keeps the moat visible and the model honest; pack size tracks usage, not features.
Packs (decided 2026-06-23)
| Pack | Price | Base credits | Bonus | Credits | Eff. ₹/credit |
|---|---|---|---|---|---|
| Starter | ₹500 | 100 | — | 100 | ₹5.00 |
| Growth | ₹1,000 | 200 | +5% | 210 | ₹4.76 |
| Pro | ₹2,000 | 400 | +10% | 440 | ₹4.55 |
- Minimum 100 credits (₹500). The bonus is a volume discount (reward cash upfront), not a feature unlock.
- Credits expire 12 months from purchase (per top-up — each has its own clock).
- Non-refundable; all purchases final.
- 1 free trial credit on signup (see Trial); bigger grants via concierge onboarding.
Unit economics (financial model, 2026-06-23)
- COGS/page: digital ~₹1.8 (Haiku extract ₹1.35 + verdict ₹0.48), CSV ₹0 (deterministic), scanned ~₹5 (Sonnet vision is 3× Haiku — roughly break-even on the ₹5 price).
- Blended COGS at an assumed 65% digital / 20% CSV / 15% scanned mix ≈ ₹1.92/credit.
- With 85% redemption (15% breakage = pure margin) and a ~2.36% domestic gateway fee, every pack holds ~62–65% contribution margin; worst realistic case (scanned-heavy, full redemption) ≈ 43%. Margin lives in the statement mix (push digital/CSV, not scanned), not the gateway.
- Bonus rule of thumb: each x% bonus costs ≈ x% × 0.33 margin points, so bonuses are safe to ~20%.
- No GST today (below the ₹20L registration threshold) — a plain bill of supply; revisit at the threshold or when an enterprise buyer requires a GST invoice (confirm with the CA).
- Per-call costs tracked live in
llm_usage;scripts/token_usage.pyprints ₹/page.
Designed for later (not built now)
The volume ICP (a mid NBFC burns far more than a ₹2k pack/month) needs: auto-recharge (top up when balance < threshold — the key retention perk, ~₹0 cost), larger packs (₹5k/₹10k at ~10–15% bonus), and GST invoicing (once registered). Build the credits flow so these slot in without rework.
When GST registration applies, also add a billing profile on the org — legal billing name,
GSTIN, and address — and use it on the receipt instead of Tenant.name (a casual display name,
which is fine pre-GST but not a valid tax-invoice party). The receipt’s “Billed to” already takes
an org name + email, so this is a data-source swap, not a redesign.
Partial processing (the differentiating rule)
If a tenant submits an M-page statement holding only N < M credits:
- Process the first N pages, debit N credits. Never reject outright (except at 0 balance).
- Total transparency: the job, report, and UI all state “N of M pages processed; M−N pages remaining” — banner on the report, flag in the contract, line in the PDF.
- Resume after recharge: partially-processed documents (and only those) offer “process the remaining pages” — extracts pages N+1…M, stitches with the stored partial ledger, re-verifies the whole chain, recomputes analytics + verdict, replaces the report, debits M−N credits.
- Re-process: fully-processed documents can be re-run (full price again). Since raw files are deleted on completion (retention promise), re-processing = re-upload.
Engineering implications (feed into #23)
| Concern | Design |
|---|---|
| Page count & debit | Count pages in the API at upload (pypdf, cheap); debit min(pages, balance); 0 balance → 402 with “buy credits” |
| Credits ledger | tenants.credit_balance (authoritative for spend) + append-only credit_transactions (grant/purchase/bonus/debit/resume-debit/expire) + credit_lots for expiry (each grant is a lot with expires_at; debits draw down soonest-to-expire first; the hourly sweep + inline-on-debit zero lapsed lots and reduce the balance). All mutations go through accounts/credits.py. Migration backfills a non-expiring lot for pre-existing balances. |
| Verify gate × partial | A truncated ledger cannot reconcile to the header’s closing balance by construction. Partial mode: verify the chain over processed pages only, skip the closing-balance check, mark integrity “partial — N of M pages”, and have analytics/verdict (and any decision run on top) carry an explicit partial-data warning (income/FOIR from partial months can mislead a credit decision — the report must say so loudly) |
| Resume mechanics | Page-range extraction already exists (chunking); stitch new pages onto the stored partial extraction (stage outputs), full re-verify, recompute, replace Result |
| Raw retention × resume | Conflict: raw deletes on done, but a recharge may come days later. Rule: partial jobs retain raw with an extended TTL (7 days, disclosed); resumed-or-expired → deleted. Fully-processed stay delete-on-done |
| Display | the app (app.obsrv.in: Analyze + Statements) shows per-doc credit cost, partial badges, and resume buttons; balance + low-credit nudge |
CSV / non-paged documents (decided 2026-06-11)
CSVs have no pages, so a billing page is defined as up to 40 transaction rows
(SP_CSV_ROWS_PER_CREDIT) — roughly what a printed statement page holds. ceil(rows / 40)
credits, minimum 1, counted at submit by the deterministic parser (unparseable CSVs are
rejected with 422 before any debit). This keeps “₹5 a page” the single price, removes the
PDF-vs-CSV arbitrage, and reuses the entire partial-processing/resume machinery unchanged
(a short balance processes the first balance × 40 rows; resume picks up the remaining
row range). Note: CSV extraction is deterministic (no LLM), so margin on CSV jobs is
near-total — encourage CSV uploads. Excel is NOT supported yet; when added (openpyxl),
the same row metering applies.
Trial
1 free trial credit on self-serve signup (decided 2026-06-11) — enough to run one page
through the full loop and see a real report, not enough to be farmable via throwaway emails.
Bigger trials happen as manual grants during concierge onboarding (scripts/grant_credits.py).
Payments provider (rate analysis 2026-06-23)
Gateway rates compared for our packs (fee on the transaction amount; sources in research notes):
| Processor | Rate | Flat fee | On a ₹500 pack | Notes |
|---|---|---|---|---|
| Cashfree | 1.6% promo¹ / 1.95% std (+18% GST) | none | ~1.9% (₹9) | cheapest; promo for new merchants before 31 Jul 2026 |
| Razorpay | 2% (+18% GST) | none | ~2.4% (₹12) | direct GST invoices when we register |
| Dodo (India MoR) | 4% | +₹4 | ~4.8% (₹24) | MoR handles tax; ~2× domestic |
| Dodo (global MoR) | 4% | +₹34 (40¢) | ~10.8% (₹54) | flat fee bleeds small INR; for USD sales only |
Key finding: Indian domestic gateways have NO flat per-transaction fee — it’s a pure ~2%, so the “a ₹5 charge bleeds” problem is a flat-fee / MoR problem, not a domestic-gateway one. At ~2% the gateway fee is a rounding error next to ~₹1.8/page COGS. The minimum pack (₹500) is for ops tidiness, not margin survival.
Lean: since we’re not GST-registered yet (below the ₹20L threshold), the main reason to pay Dodo’s ~4% (its MoR handling our tax) doesn’t apply — so a domestic gateway (Cashfree promo, else Razorpay) is both cheaper and simpler now. The one tradeoff: Dodo’s individual onboarding is faster to launch (no LLP KYC / live-website gate), while Razorpay/Cashfree need LLP KYC. So: pick domestic if onboarding KYC is ready; otherwise Dodo is an acceptable fast-start at ~4%. Dodo stays the natural rail for future international (USD) sales, where MoR tax handling earns its fee.
The swap is trivial by design: payments touch the system only as “webhook → credit grant”, so any provider is a plug. Credits grant ONLY on the payment-succeeded webhook.
Implementation (built 2026-06-23)
The purchase infra is live and provider-agnostic — a real gateway drops in by implementing one
interface; the mock provider runs the whole flow end-to-end today:
- Gateway factory
src/core/payments/—PaymentGatewayprotocol (create_checkout+parse_webhook), aregistry/get_gateway()selectingSP_PAYMENT_PROVIDER(defaultmock), andMockGateway(HMAC-verified, full flow). Add Razorpay/Cashfree/Dodo = one class +register. - Pack catalog
src/accounts/packs.py(the 3 packs, ₹5/credit, 365-day expiry). - Routes
GET /v1/credits/packs,POST /v1/credits/checkout(records a pendingcredit_purchasesrow, returns the gateway URL; reuses a recent open purchase for the same tenant+pack so an accidental double-click can’t double-pay),POST /v1/credits/webhook/{provider}(signature-verified, idempotent grant of base + bonus lots, emails a receipt), and a dev-onlyPOST /v1/credits/mock/complete. - Receipt
accounts/templates/receipt.html+ a PDF copy attached (receipt_pdf.py, reportlab); bonus split shown only when non-zero. Failure to send never voids a paid purchase. - Buy UI on
/credits(the three packs; mock provider completes inline; a synchronous lock blocks double-submit).
¹ Cashfree 1.6% is a new-merchant promo locked for 12 months from signup (before 31 Jul 2026).
Future lever: storage / retention tiers (not decided)
Analysis is priced per page; retained originals are a separate cost that today is free for
retain-mode (vault) orgs. We now track it: each job stores size_bytes, and
GET /v1/statements/usage returns per-tenant lifetime + currently-retained bytes (and counts).
That instrumentation is the basis for a future decision if storage cost grows — e.g. a per-tier
cap on number/size of retained documents, or a small storage add-on above a free allowance.
Nothing is gated today; this just records that the data exists to gate it when needed.