Skip to Content
ProductPricing

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)

PackPriceBase creditsBonusCreditsEff. ₹/credit
Starter₹500100100₹5.00
Growth₹1,000200+5%210₹4.76
Pro₹2,000400+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.py prints ₹/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:

  1. Process the first N pages, debit N credits. Never reject outright (except at 0 balance).
  2. 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.
  3. 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.
  4. 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)

ConcernDesign
Page count & debitCount pages in the API at upload (pypdf, cheap); debit min(pages, balance); 0 balance → 402 with “buy credits”
Credits ledgertenants.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 × partialA 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 mechanicsPage-range extraction already exists (chunking); stitch new pages onto the stored partial extraction (stage outputs), full re-verify, recompute, replace Result
Raw retention × resumeConflict: 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
Displaythe 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):

ProcessorRateFlat feeOn a ₹500 packNotes
Cashfree1.6% promo¹ / 1.95% std (+18% GST)none~1.9% (₹9)cheapest; promo for new merchants before 31 Jul 2026
Razorpay2% (+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/PaymentGateway protocol (create_checkout + parse_webhook), a registry/get_gateway() selecting SP_PAYMENT_PROVIDER (default mock), and MockGateway (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 pending credit_purchases row, 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-only POST /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.