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:
- Active — credits the customer can spend right now. Subscription period grant + unfrozen top-ups.
- Frozen — top-up credits locked while the customer has no active subscription. Visible in the wallet, unspendable. Unfreezes instantly on the next subscription purchase.
- 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
| Mode | What 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:
- Engine reaches the LLM node
- Estimate credits from prompt size (chars / 4 × 1.2 safety buffer + 4096
output-token estimate) converted via
tokens_per_creditworkspace setting - Pre-deduct estimate from balance — returns
402 payment_requiredif insufficient (not retried) - Run LLM call
- Reconcile to actual token usage: refund excess or charge shortfall
- On node failure: full refund of the pre-deduction
Deduction Order
available = active_credits # subscription + unfrozen top-upssub_deduct = min(cost, active_subscription_credits)topup_deduct = max(cost - sub_deduct, 0) # oldest top-up grant firstEach 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-a32abf613c76The 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 }}| Field | Meaning |
|---|---|
active_credits | Spendable right now. Sum of unfrozen subscription + top-up grants. |
frozen_credits | Top-ups locked behind a lapsed subscription. Render with a lock icon + “Resubscribe to unlock your X credits.” |
inactive_credits | Voided / refunded grants. Audit only — never sum into the headline balance. |
next_refresh_at | Next subscription period end or next annual monthly-tranche refresh. null when there is no active subscription. |
is_premium | Whether Athena’s current active entitlement grants premium access. |
subscription_product_identifier | Athena 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_at | Timestamp 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-dismissX-API-Key: eak_...Idempotent. Returns 204 No Content. Backend writes
external_customer.frozen_card_dismissed_at = now().
Force a Balance Refresh
GET /my/billing/refreshX-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/dismissX-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:
| Setting | Purpose |
|---|---|
| Webhook Secret | Verifies inbound RevenueCat webhooks. |
| Project ID | Used in RevenueCat v2 /projects/{project_id}/customers/{customer_id}/subscriptions calls. |
| Secret REST API Key | Server credential for v2 customer subscription and product lookups. Do not use the public SDK key. |
Key behaviors:
| Event | Effect |
|---|---|
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. |
RENEWAL | Forfeit the previous period grant, create the new one. Idempotent top-up unfreeze. |
NON_RENEWING_PURCHASE | Same as consumable initial purchase. |
PRODUCT_CHANGE | Informative only. Never grants or modifies credits. Wait for the companion RENEWAL / INITIAL_PURCHASE. |
EXPIRATION | Freezes all top-ups atomically. Subscription credits finish their current period and then forfeit. |
BILLING_ISSUE | Grace 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
| HTTP | Code | Meaning |
|---|---|---|
402 | payment_required | Active balance insufficient. Message: "insufficient_credits" |
403 | forbidden | Calling key has no external customer attached |
503 | service_unavailable | Credit service not configured for the workspace |
Reliability Guarantees
| Guarantee | Mechanism |
|---|---|
| No double-charge on crash | 5-minute heartbeat job refunds unreconciled pre-deductions on stale executions |
| Webhook dedup | event.id Redis set with 30-day TTL + credit_grant.rc_event_id unique index |
| Concurrent writes | Append-only ledger; balance computed at read-time |
| Refund clawback | Voids 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_creditsreads withactive_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_creditsreads withactive_credits+frozen_creditsdepending on what you want to surface.frozen_credits > 0while no active subscription is the new “locked top-ups” state. - Keep using
is_premiumandsubscription_product_identifierfrom/my/creditsfor Athena-gated app behavior. If the client just changed RevenueCat identity or purchase state, call/my/billing/refreshfirst 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.
Related Docs
- Webhooks — RevenueCat event setup
- Execute — credit balance endpoint and
X-Credits-Remainingheader - Error Codes —
payment_requireddetails