Skip to content

Finance-grade credit/usage attribution (per-request credit + payment ledger) #704

@PierreLeGuen

Description

@PierreLeGuen

Problem

The platform analytics endpoints added in #702 cannot produce finance-grade revenue or money-in numbers, because cloud-api does not record the financial facts needed:

  1. No per-request credit attribution. organization_usage_log records what was consumed but not which credit funded it (grant vs payment, and which source). A first attempt classified an org by whether it currently has a payment credit and applied that to all history — which is time-inconsistent (past grant burn flips to "paid" after conversion). That split was removed in Add executive stats analytics endpoints #702.
  2. No payment/credit ledger. organization_limits_history.spend_limit is a cap (ceiling), not a top-up/transaction record. There is no payments/transactions/refunds table in cloud-api; actual payments live in the billing service (Stripe via nearai-cloud-ui). So "money in", deferred revenue, and ARR run-rate cannot be derived here. The billing-summary endpoint was reduced to credit limits + consumption in Add executive stats analytics endpoints #702.

Goal

Make paid-vs-granted revenue and money-in accurate and auditable.

Proposed work (for discussion)

A. Per-request credit attribution (debit side)

  • Add columns to organization_usage_log: funded_credit_type (grant|payment), funded_source (e.g. stripe, nearai, hot-pay), and optionally price_version.
  • Populate at write time in the usage recording path (record_usage) using an explicit drain order (e.g. grants before payments) so each request maps to the bucket it actually drew from.
  • Backfill historical rows as unknown.

B. Credit/payment ledger (credit side)

  • Introduce a real transaction ledger (payments, grants, refunds, expirations) — either in cloud-api (synced from the billing service webhooks) or owned by the billing service and queried/exposed for analytics.
  • Distinguish provisioned vs consumed vs refunded/expired per credit type/source.

C. Analytics

  • Once A/B exist, restore paid-vs-granted revenue (from attribution), add real money-in/deferred-revenue, and consider daily/hourly rollups for the dashboard.

References

  • PR Add executive stats analytics endpoints #702 (analytics endpoints; dropped the unsound split, reduced billing-summary to caps + consumption).
  • Billing service: nearai-cloud-ui (lib/webhook-utils.ts, Stripe webhook + credit sync).
  • cloud-api/crates/database/src/repositories/organization_usage.rs (record_usage), migrations/sql/V0004*, V0044* (organization_limits_history credit_type/source).

Out of scope

Dashboard UI changes (tracked with the admin-ui work).

Add: postpaid / invoiced customers (a third credit type)

Some orgs are postpaid — we extend NEAR AI credits now and invoice actual usage quarterly / end-of-month (net terms). Today the model only has grant | payment, so these get recorded as grant and are miscounted as free in any "paying customers" / revenue view. They are real revenue, just billed in arrears.

Decision: model them as a new credit_type = 'postpaid' (not an org-level flag, not a source hack) — consistent with the existing model; credit_type is VARCHAR(50) with no CHECK, so no constraint migration.

Scope to include in this ledger work:

  • Add CreditType::Postpaid (enum in crates/api/src/models.rs, admin parse in admin.rs, column comment in a migration).
  • Treat "paying / revenue-generating" = credit_type IN ('payment','postpaid') everywhere it's derived (analytics.rs: paying count, billing-summary, org-revenue paying CTE).
  • Recognized vs billed diverges for postpaid: revenue is recognized on consumption (already in organization_usage_log); cash-in happens later via invoice — the ledger must record invoices/payments for these orgs, and spend_limit for them is a risk ceiling, not money-in.
  • One-off backfill of the few existing postpaid orgs from grant → postpaid.
  • The existing unique-active-per-(org, credit_type) index already supports an org having both a free grant and a postpaid arrangement — no index change.

Add: historical stability of the verifiable / provider split

The verifiable-vs-non-verifiable (and provider-type) breakdowns are computed by joining
organization_usage_log to the current models row (verifiable, provider_type). If a model's
verifiable flag or provider type changes later, historical periods shift retroactively — the same
class of bug we removed for paid-vs-granted. Today it's only labeled in the API/UI as "current model
metadata applied to historical usage."

Fold into this work: snapshot model metadata at usage time — store per usage row (or in the per-request
charge fact) the model id, canonical name, provider_type, and verifiable/attestation class (and ideally
price version), so verifiable-share and provider-mix are historically stable for an exec KPI.

Metadata

Metadata

Assignees

No one assigned

    Labels

    businessrfcDesign/architecture RFC

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions