Skip to content

Credits

Athena meters paid AI usage through a three-bucket credit ledger per customer. Credits gate LLM usage; rate limits gate request volume. They are independent.

If you’re upgrading from an older single-balance integration (one subscription_credits / consumable_credits pair), see the migration notes at the bottom of this page.


TL;DR

Three buckets per customer:

  1. Active — credits the customer can spend right now. Subscription period grant + unfrozen top-ups.
  2. Frozen — top-up credits locked while the customer has no active subscription. Visible in the wallet, unspendable. Unfreezes instantly on the next subscription purchase.
  3. Inactive — voided or refunded grants kept for audit. Never displayed in the headline balance.

Spending rules:

  • Subscription credits are consumed before top-up credits.
  • Top-up credits are consumed in FIFO order within the active bucket.
  • Spending is blocked when the customer has no active subscription — even if they have unfrozen top-ups in the wallet.

Per LLM call the engine pre-deducts an estimate, runs the node, then reconciles to actuals (refunds excess or charges shortfall). On node failure the original deduction is fully refunded.


How Credits Are Consumed

Two Agent Modes

ModeWhat is charged
Fixed-cost (base_credits set on agent)Flat credit amount, deducted before engine starts
Token-mode (base_credits not set)Per-LLM-node: estimate → deduct → run → reconcile

Pre-Deduct, Run, Reconcile

For token-mode agents, every AI node follows this lifecycle:

  1. Engine reaches the LLM node
  2. Estimate credits from prompt size (chars / 4 × 1.2 safety buffer + 4096 output-token estimate) converted via tokens_per_credit workspace setting
  3. Pre-deduct estimate from balance — returns 402 payment_required if insufficient (not retried)
  4. Run LLM call
  5. Reconcile to actual token usage: refund excess or charge shortfall
  6. On node failure: full refund of the pre-deduction

Deduction Order

available = active_credits # subscription + unfrozen top-ups
sub_deduct = min(cost, active_subscription_credits)
topup_deduct = max(cost - sub_deduct, 0) # oldest top-up grant first

Each credit_consumption row records which grants the spend pulled from so reconciliation and refunds can reverse the exact ledger movement.


Credit Balance API

GET /my/credits[?generation_cost=N]
X-API-Key: eak_...
X-Workspace-Id: 019f58c6-0400-7fa2-a41f-a32abf613c76

The optional generation_cost triggers the depletion bridge — a recommended top-up pack just big enough to cover the requested cost.

/my/credits reads Athena’s local ledger and active entitlement state. It does not call RevenueCat. Use it as the app’s normal wallet/premium-status read path.

{
"data": {
"active_credits": 87.4,
"frozen_credits": 25.0,
"inactive_credits": 0.0,
"next_refresh_at": "2026-05-21T10:00:00Z",
"is_premium": true,
"subscription_product_identifier": "weekly_pro_v7",
"recommended_topup_pack_id": "topup_pack",
"recommended_topup_pack_credits": 150,
"recommended_topup_pack_currency": "USD",
"frozen_card_dismissed_at": null
}
}
FieldMeaning
active_creditsSpendable right now. Sum of unfrozen subscription + top-up grants.
frozen_creditsTop-ups locked behind a lapsed subscription. Render with a lock icon + “Resubscribe to unlock your X credits.”
inactive_creditsVoided / refunded grants. Audit only — never sum into the headline balance.
next_refresh_atNext subscription period end or next annual monthly-tranche refresh. null when there is no active subscription.
is_premiumWhether Athena’s current active entitlement grants premium access.
subscription_product_identifierAthena catalog product ID for the active subscription, normally the same value as the RevenueCat store product identifier.
recommended_topup_pack_*Populated only with ?generation_cost=N and when the customer is eligible to buy top-ups. Use the id/credits/currency fields together to render the out-of-credits CTA; resolve the price client-side from the store.
frozen_card_dismissed_atTimestamp when the customer last dismissed the frozen-card banner. Use to decide whether to re-show on this session.

Dismiss the Frozen-Card Banner

POST /my/billing/frozen-card-dismiss
X-API-Key: eak_...

Idempotent. Returns 204 No Content. Backend writes external_customer.frozen_card_dismissed_at = now().

Force a Balance Refresh

GET /my/billing/refresh
X-API-Key: eak_...

When RevenueCat is enabled, this endpoint first performs a live RevenueCat v2 lookup by the customer’s app_user_id / external_id, reconciles Athena’s active entitlement, busts the server-side balance + status caches, reads fresh grants, and returns the same shape as /my/credits. The depletion-bridge fields are always null here.

Use it after Purchases.logIn(...), purchase, restore, refund, plan change, or app foreground when the client needs authoritative billing state instead of the normal cached local read. Rate-limited to 1 request per minute per customer (429 on excess).

Dismiss the Discount Modal

POST /my/paywall/dismiss
X-API-Key: eak_...
Content-Type: application/json
{ "tier": "pro", "duration": "annual" }

Call this when the customer dismisses the discount/paywall modal a second time. Returns 204 No Content and schedules a single re-engagement push to fire 24–48h later; the tier / duration you send are echoed back in the push payload so the deep link can reopen the same modal. Stacking-guarded — repeated dismisses while a push is already pending do not enqueue duplicates.


Trials and Free Credits

Athena does not mint workspace-configured free credits, free tokens, or an Athena-managed free-trial balance. Customers without an active subscription are blocked with 402 subscription_required; historical FreeTier grant rows are kept for audit but are not spendable.

Trial and intro access comes from App Store / Play products managed through RevenueCat. When RevenueCat sends an INITIAL_PURCHASE with period_type = TRIAL or INTRO, Athena follows the normal subscription purchase path: creates the entitlement, grants the product catalog credits, and schedules the T-1d trial reminder. Trial length and eligibility live in the store product / RevenueCat configuration.

next_refresh_at reflects the subscription period / tranche boundary. To show a fresh number immediately after a purchase or refund, call GET /my/billing/refresh rather than waiting for the cache to expire.


RevenueCat Integration

Athena listens for RevenueCat webhook events to manage subscription state and credit grants. See the Webhooks guide for the full event matrix and setup instructions.

Athena also uses RevenueCat v2 for live repair. Configure all three values in Workspace Settings → Subscription Providers:

SettingPurpose
Webhook SecretVerifies inbound RevenueCat webhooks.
Project IDUsed in RevenueCat v2 /projects/{project_id}/customers/{customer_id}/subscriptions calls.
Secret REST API KeyServer credential for v2 customer subscription and product lookups. Do not use the public SDK key.

Key behaviors:

EventEffect
INITIAL_PURCHASE (subscription)Grant first period (or first annual monthly tranche). Unfreezes all top-ups.
INITIAL_PURCHASE (consumable)Add top-up credits. If no active sub, the grant lands in the frozen bucket.
RENEWALForfeit the previous period grant, create the new one. Idempotent top-up unfreeze.
NON_RENEWING_PURCHASESame as consumable initial purchase.
PRODUCT_CHANGEInformative only. Never grants or modifies credits. Wait for the companion RENEWAL / INITIAL_PURCHASE.
EXPIRATIONFreezes all top-ups atomically. Subscription credits finish their current period and then forfeit.
BILLING_ISSUEGrace period. Credits remain spendable.
CANCELLATION (refund)Clawback via rc_transaction_id. Balance clamps at zero — never negative.

Credits are only mutated when credits_enabled = true for the workspace. Subscription state transitions always run regardless.


Plan-Change Proration

Apple and Google handle proration asymmetrically:

  • Apple cash-refunds the unused-time portion of the old plan to the user’s Apple ID balance. The credit ledger voids unused grants to avoid double-compensation.
  • Google issues no cash refund but appends bonus days to the new subscription term. The credit ledger carries unused top-up credits forward into the new period.

Over-consumption (using more than the time-prorated share before upgrading) is deducted from the new plan’s allowance on both stores. The default policy is HARD_DEDUCT; admins can flip it to BLOCK_UPGRADE per workspace.

Before opening a store purchase sheet for an existing subscriber, call Plan Change Preview. The endpoint resolves the current subscription, store, direction, replacement mode, credit effect, charge, timing, and rate-limit state from backend data.

For Google, the purchase call still needs the current oldProductId, but the replacement mode must come from preview’s google_proration_mode. Do not derive the mode locally from the transition.


Error Handling

HTTPCodeMeaning
402payment_requiredActive balance insufficient. Message: "insufficient_credits"
403forbiddenCalling key has no external customer attached
503service_unavailableCredit service not configured for the workspace

Reliability Guarantees

GuaranteeMechanism
No double-charge on crash5-minute heartbeat job refunds unreconciled pre-deductions on stale executions
Webhook dedupevent.id Redis set with 30-day TTL + credit_grant.rc_event_id unique index
Concurrent writesAppend-only ledger; balance computed at read-time
Refund clawbackVoids only the affected grants via rc_transaction_id; balance clamps at zero

Migration from the two-bucket model

If you previously read subscription_credits / consumable_credits / is_premium from /my/credits:

  • Replace subscription_credits reads with active_credits (subscription credits are the first slice consumed from the active bucket; you typically do not need to split them out client-side).
  • Replace consumable_credits reads with active_credits + frozen_credits depending on what you want to surface. frozen_credits > 0 while no active subscription is the new “locked top-ups” state.
  • Keep using is_premium and subscription_product_identifier from /my/credits for Athena-gated app behavior. If the client just changed RevenueCat identity or purchase state, call /my/billing/refresh first so Athena reconciles from RevenueCat v2 before reading /my/credits.
  • Add the frozen-card banner state machine + POST /my/billing/frozen-card-dismiss.
  • Add the depletion-bridge query (?generation_cost=N) before high-cost generations.

  • Webhooks — RevenueCat event setup
  • Execute — credit balance endpoint and X-Credits-Remaining header
  • Error Codespayment_required details