Billing

Every organization on Autonoma spends credits. One ledger tracks them, one package (@autonoma/billing) owns every read and write to it, and two settlement rails put money behind them: Stripe for direct customers, the Vercel Marketplace for organizations that installed Autonoma from Vercel.

The distinction that matters most, and the one most often gotten wrong: the two rails are separate at the money layer and identical at the credit layer. A Vercel organization’s credits live in the same column, move through the same deduction code, and are granted by the same function as a Stripe organization’s. Only the settlement differs.

The ledger

BillingCustomer.creditBalance is the single authoritative total, one row per organization. CreditTransaction is the append-only history behind it - one row per event, signed (negative for consumption, positive for grants), each carrying the balance it produced.

subscriptionCreditBalance sits beside it and is not a second pool. It is a tag recording how much of creditBalance came from the current plan cycle. “Top-up credits” in the UI is derived, not stored: creditBalance - subscriptionCreditBalance.

Idempotency

Nothing in this package asks “have I already processed this?”. Instead every write derives a deterministic transaction id from whatever it is charging for, and lets the primary key collide:

PathTransaction id
Test generationctr_gen_${generationId}
LLM proxy requestctr_llm_${requestId}
AI cost batchctr_ai_${firstAiCostRecordId}
Preview runtime windowctr_preview_${usageWindowId}
Previewkit buildctr_build_${appBuildId}

The insert is ON CONFLICT (id) DO NOTHING, and the balance UPDATE in the same statement is gated on EXISTS (SELECT 1 FROM inserted) - so a retry cannot double-charge even though the two writes are separate clauses. Stripe-anchored grants use unique foreign-key columns instead (stripePaymentIntentId, stripeInvoiceId, stripeRefundId).

Spending credits

Five paths consume credits. Three share one primitive; two predate it.

deductCreditsFloored is the shared primitive. It never refuses: it clamps the balance at the organization’s creditFloor (default 0) instead of requiring sufficient funds, because work already in flight must never be half-billed.

PathPriced byUses the primitive
AI cost (analysis activities)credits-per-USD rateyes
Preview runtime computecreditsPerVcpuHour / creditsPerGbMemoryHouryes
Previewkit build computesame compute ratesyes
Test generationper-architecture generation costno - hard sufficiency gate that throws
LLM proxy (planner CLI)credits-per-USD rateno - floors at literal 0

The bottom two rows are historical accidents rather than intent.

AI cost attribution

AI-consuming activities never pass an organization id. persistAiCosts reads it from the ambient observability context, bound by the Temporal activity interceptor from the activity’s own input. A new AI-consuming activity therefore gets correct attribution and billing with no call-site wiring.

The deduction is best-effort: a pricing or database failure logs and moves on, because a billing side-effect must never sink an activity that already produced its real result.

Compute metering

Running previews and build jobs are measured differently because they have to be. A live preview’s usage is read from Prometheus in fifteen-minute windows by the preview-usage-meter cronjob. Build jobs cannot be read the same way - buildkit nodes are excluded from the scrape - so build usage is derived from the build’s duration times its node’s known shape.

Both are priced with the same per-hour rates, and those rates are checked against real AWS pricing: a weekly job derives what the underlying instances actually cost, blending on-demand and spot by buildkit’s genuine recent mix, and alerts when the reference drifts more than 10% from what was stored. It only ever writes a reference table - a human decides whether to move any organization’s live rate.

Both compute rates default to zero, so compute is metered but not charged until an admin sets them.

Getting credits

  • Top-up purchases - a fixed-size credit pack bought through Stripe Checkout.
  • Auto top-up - optional per organization: when the balance falls below autoTopUpThreshold, charge the saved card for another top-up automatically.
  • Promo codes - redeemable once per organization, with optional redemption limits and date windows.
  • Free start credits - a one-time grant keyed on the email address, not the organization, so creating a second organization does not earn a second grant.
  • Refunds - a refunded purchase revokes credits proportionally.

Guardrails

Credit floor - how far below zero an organization’s balance may go. Work already running keeps going and is charged in full; the floor only blocks new work starting, via checkPreviewDeployCreditsGate (preview deploys) and checkAnalysisCreditsGate (PR analysis runs).

Grace period - an unpaid invoice starts a countdown on either rail. Once it expires, the gates begin refusing work.

Note that autoTopUpThreshold is a floor that triggers a purchase, not a ceiling on spending. It is easy to mistake for one.

The two rails

StripeVercel Marketplace
Recurring creditsSubscription invoicePlan allotment per cycle
Extra creditsBuy a top-up via CheckoutOverage minted internally, billed in arrears
Spending ceilingnoneOverage cap, per plan cycle
Auto top-upYes, needs a saved cardNo

Both rails call the same grant function when an invoice is paid, and the plan allotment for a Vercel organization is copied into the same pricing column a Stripe subscription uses. On both rails that grant is a reset, not an addition - each cycle overwrites the previous allotment and records the unused remainder as forfeited, so plan credits do not roll over.

On the Vercel rail, an unset overage cap means a hard stop at the plan allotment, not “unlimited”.

Self-hosting

Setting STRIPE_ENABLED=false swaps the whole package for an implementation where every gate returns “allowed” and every deduction is a no-op. Self-hosted deployments are not metered.

Known gaps

Documented because they are real, not because they are acceptable:

  • Vercel invoice submission is not atomic across the network call, so a submitted-then-crashed run can invoice the same period twice.
  • Refunding a Vercel invoice does not claw back the credits it granted.
  • An organization can hold more than one active Vercel installation; reads arbitrarily pick the newest.
  • Overage is consulted only by the test-generation gate, so a Vercel organization with overage enabled is still blocked from preview deploys and analysis runs at its floor.
Link copied