> For the complete documentation index, see [llms.txt](https://docs.auray.ai/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.auray.ai/documentation/credits-and-plans.md).

# Credits and plans

{% hint style="danger" %}
**The public API is switched off on this deployment.** `API_V1_ENABLED` is unset, so every `/v1` route answers `503 api_disabled` — refused before your key is parsed, which is why a valid key and a malformed one look identical there. There is no key that gets past it. Everything below is what the code does, and what the desktop charges today.
{% endhint %}

## One wallet, five generators

Photos, music, video, 3D, chat and the Code sandbox all spend the same balance. There is no per-product allowance and there is not going to be one.

The reason is a specific failure rather than a preference. `grant_plan_credits` is idempotent on the key `grant:<user>:<YYYY-MM>` and ignores its amount argument on the second call of a month. Add a second monthly number beside `creditsPerMonth` and whichever app you happened to open first that month decides your entire balance — non-deterministically, and only in the months where the order differs. `creditsPerMonth` is therefore a property of the account, not of a product.

## What a credit is worth

**$0.024.** All five products anchor on that single number, and it is Ultimate's rate — the cheapest on the ladder. Pricing everything against the cheapest rate is the conservative choice: every other plan earns more than the tables below claim.

| Plan     | Per month | Credits granted | What you pay per credit |
| -------- | --------- | --------------- | ----------------------- |
| Free     | $0        | 0               | —                       |
| Pro      | $20       | 600             | $0.0333                 |
| Max      | $60       | 2,000           | $0.0300                 |
| Ultimate | $120      | 5,000           | $0.0240                 |

The rule every price in the repository is held to: credits granted × $0.024 is at least twice the GPU cost of whatever they are spent on. Where a case falls short of that, it is written down beside the constant rather than rounded away — a music job that pays a full cold start earns about 1.88x, a fully cold 3D job about 1.3x, and both are accepted because the alternative prices a warm job at eight to twenty times cost.

{% hint style="warning" %}
**Stripe is in test mode.** Checkout and cancellation are real flows and they do change your tier, but only test cards are accepted and nothing is charged. Your tier is what decides what Video, Photos, Music, 3D, Messages and Code will do, and the routes enforce it rather than only the buttons.
{% endhint %}

### The balance carries over, capped at three months

Credits do not expire. They also do not pile up without limit: twelve months of Ultimate would accrue 60,000 credits, $1,440 of liability spendable in a weekend.

The grant is trimmed rather than the balance truncated, and the difference matters:

```
delta := greatest(0, least(grant, 3 * grant - balance))

  balance  1,000 + 600 -> 1,600   full grant
  balance  1,700 + 600 -> 1,800   capped, partial grant of 100
  balance 14,000 + 600 -> 14,000  no grant, and nothing taken
```

Written the obvious way — `balance := least(balance + grant, 3 * grant)` — an Ultimate subscriber holding 14,000 credits who downgraded to Pro would have their next grant compute 1,800 and steal 12,200 credits they had already paid for. The form above can only ever add.

When the cap blocks a grant entirely the period key is **not** claimed, so an account that spends back down inside the same month gets its grant then. Still one grant per month; simply deferred until there is room.

### The grant lands on a generate, not on a clock

There is no cron. `pg_cron` is not installed, and the two scheduled jobs this deployment declares grant nothing. `grant_plan_credits` runs on the generate path and when the desktop reads your credits, which is why the balance moves the first time you ask for something in a new month rather than at midnight on the first.

`GET /v1/me` deliberately does not grant. A grant on every GET is a write transaction taking a row lock on one wallet row, and under a client retry loop that is tens of write transactions a second against the connection pool the desktop shares. It reads, and nothing else.

## What each plan includes

|                           | Free | Pro                    | Max           | Ultimate                                      |
| ------------------------- | ---- | ---------------------- | ------------- | --------------------------------------------- |
| Credits per month         | 0    | 600                    | 2,000         | 5,000                                         |
| Balance ceiling           | —    | 1,800                  | 6,000         | 15,000                                        |
| Photo: images per job     | —    | 2                      | 4             | 4                                             |
| Photo: largest resolution | —    | 1024                   | 2048          | 2048                                          |
| Photo: jobs at once       | 0    | 1                      | 3             | 3                                             |
| Music: longest song       | —    | 60 s                   | 180 s         | 300 s                                         |
| Music: vocals             | —    | yes                    | yes           | yes                                           |
| Music: jobs at once       | 0    | 1                      | 2             | 3                                             |
| Video: tiers              | —    | `fast`, `pro`          | `fast`, `pro` | `fast`, `pro`                                 |
| Video: longest clip       | —    | 8 s                    | 12 s          | 15 s                                          |
| Video: jobs at once       | 0    | 1                      | 2             | 2                                             |
| Video: `upscale_2k`       | —    | —                      | —             | —                                             |
| 3D: tiers                 | —    | `geometry`, `textured` | + `rigged`    | + `controlled-object`, `controlled-character` |
| 3D: jobs at once          | 0    | 1                      | 2             | 3                                             |
| Chat: output ceiling      | —    | 4,096 tokens           | 8,192         | 16,384                                        |
| Chat: history re-sent     | —    | 24,000 tokens          | 64,000        | 160,000                                       |
| Code: projects            | 0    | 5                      | 20            | 50                                            |

**Vocals come from Pro rather than Max.** Instrumental and sung cost exactly the same to run, and gating a cost-free axis is what makes a price list feel arbitrary. The licence's anti-abuse concern is met by "paid plan, card on file, identity attached", which Pro already is.

**`rigged` is held back from Pro, and not to create an upsell.** It is the tier most likely to fail on the wrong input — UniRig dies on furniture — and a plan's first 3D job should not be the one that teaches that.

**Ultimate's 3D concurrency is 3, not 4.** The whole platform has four GPU containers for 3D, and a per-user allowance at the global cap lets one account hold every container on the platform.

**`upscale_2k` is refused on every plan, Ultimate included.** The 2K pass is a call out to a hosted service this deployment has no key for, so the request schema refuses it at validation with `403 upscale_not_in_plan`. Leaving it enabled would charge the surcharge, fail at submit and refund — a round trip through your wallet for a feature that cannot run. It is not an upgrade prompt; drop the flag.

### Free is a read-only account

`ALLOWANCE.free.creditsPerMonth` is 0, and every product gates on that directly or on a table that is empty for the same reason. Six of the sixteen desktop apps therefore do nothing on a free account, and a free API key cannot start a render at all.

The refusal is `403 plan_has_no_api_generate`, not a `429` and not a `402`. The `generate` bucket has a capacity of zero, which is not the same as being out of tokens: a bucket of zero never refills into one, so retrying can never help and the error says so. What a free key can still do is read jobs and fetch assets from work created earlier.

## What each product costs

Prices, not costs. Every one of them is derived in code from measured GPU seconds against a published rate, and the derivations sit beside the constants.

### Photo

A flat base of **3 credits** per job, plus per image:

| Resolution | Per image | 1 image | 4 images |
| ---------- | --------- | ------- | -------- |
| 1024       | 1         | 4       | 7        |
| 2048       | 5         | 8       | 23       |

The base exists because a cold container pays about 36 seconds of weight loading that no per-image rate can cover — on the rate alone a cold-started single 1K image bills less than it costs. Two credits was the first answer and put that case at 0.98x, selling below cost in the one case that happens most on a quiet service. At three, every combination clears cost by at least 1.4x even if every job is cold.

2K is five credits rather than two because it costs 4.94x what 1K costs. A 2:1 price against a 4.94:1 cost is 1K quietly subsidising 2K, which is not a discount — it is a mispricing that gets worse the more people notice it.

### Music

**2 credits, plus one for every four seconds of audio requested.**

| Duration | 10 s | 30 s | 60 s | 180 s | 300 s |
| -------- | ---- | ---- | ---- | ----- | ----- |
| Credits  | 5    | 10   | 17   | 47    | 77    |

One credit bought six seconds until our own renders were measured. The service publishes 1.15x realtime; through this stack it ran at 1.78x, which put a five-minute song at 1.31x cost and falling — getting worse the more the customer bought. Four seconds per credit is the nearest whole-second block that fixes it, and it lands the longest song at 1.99x.

The charge is against the **requested** duration, which is known before dispatch and is what you agreed to. `duration_seconds` is a target rather than a promise, so a short song refunds the difference afterwards.

### Video

`fast` and `pro`. A third tier, `standard`, was retired: it ran the same 50 steps at the same quality as `pro` on slower hardware, so it was 3.2x slower, cost 15% more to run and charged 14% more for identical output.

| Seconds | `fast` | `pro` |
| ------- | ------ | ----- |
| 4       | 6      | 21    |
| 5       | 8      | 29    |
| 8       | 16     | 63    |
| 12      | 30     | 122   |
| 15      | 42     | 172   |

A table rather than a formula, so the number you are charged can be read and audited rather than trusted. Render time grows with roughly `duration^1.63`, and a linear price does not: at a flat 6 credits a second, a 15-second clip billed $2.16 and cost $2.35 to render.

`fast` is a different product rather than a slower one — 8 steps against 50, and roughly a quarter of the price. It is also the restricted one: the adapter behind it takes no reference images and no dialogue.

### 3D

Priced by tier, and each tier is a fixed set of stages:

| Tier                   | Stages                 | Credits |
| ---------------------- | ---------------------- | ------- |
| `geometry`             | geometry               | 5       |
| `textured`             | geometry, texture      | 11      |
| `rigged`               | geometry, texture, rig | 14      |
| `controlled-object`    | geometry, texture      | 17      |
| `controlled-character` | geometry, texture, rig | 20      |

The rig stage is added as one shared delta of 3 credits to both rigged tiers rather than derived per tier. `controlled-object` and `controlled-character` differ by 1.2 credits before rounding, and the paint measurement they both rest on is n=5 with a 48.9–156.0 second spread. A mean that drifts a few seconds is enough to cross them, and a ladder where the character tier came out cheaper would steer users straight into a documented failure — sending furniture to `controlled-character` fails the whole job inside UniRig.

The upstream publishes its own credit numbers in its catalogue. They are placeholders its own docstring disowns, and reading them would give this platform a second price list that can drift from the one it charges.

### Chat

Chat is the only product that reserves and then settles down, because it is the only one whose price is not knowable from the request.

The reserve is priced against `max_tokens` — the ceiling the model was permitted to reach, which is honoured: a run capped at 64 returned exactly 64 completion tokens with `finish_reason: "length"`. The settle uses the authoritative `usage` object that comes back.

| Model  | Input        | Cached input | Output        |
| ------ | ------------ | ------------ | ------------- |
| `qwen` | $2.00 / MTok | $0.25 / MTok | $6.00 / MTok  |
| `kimi` | $3.00 / MTok | $0.30 / MTok | $15.00 / MTok |

Both numbers are reported, so you can see the difference:

```json
{
  "usage": { "prompt_tokens": 52, "completion_tokens": 104 },
  "credits_reserved": 4,
  "credits_charged": 2,
  "credits_remaining": 573
}
```

Measured on the first API turn through this surface: reserved 4, settled 2.

Two things the settle deliberately does. It does not add `reasoning_tokens` — they are a subset of `completion_tokens`, not an addition to them, and adding them is a straight double charge. And it does not ignore the prompt cache: on the second turn of an ordinary conversation, 256 of 289 prompt tokens came back already cached, and a settle blind to that overcharges every multi-turn thread by close to an order of magnitude on its input term.

There is a floor of one credit per turn. Not a container cost — a shared endpoint has no cold start we pay for — but `consume_credits` raises on a non-positive amount, and a settle that could reach zero would make "delivered a full answer, paid nothing" a representable outcome.

If your balance cannot fund the plan's full output ceiling, the reserve **shrinks** rather than refusing. Otherwise a user holding 9 credits against a 10-credit ceiling would be refused a two-word question that settles at 1, and every month would end with a ceiling-wide band of credits nobody could spend.

### Spent from the same wallet, with no `/v1` route

The Code sandbox charges 3 credits per five minutes of runtime — 36 an hour, which is about 17 hours on Pro's monthly grant if credits went nowhere else. Narration charges 20 credits per thousand characters, because it is a call out to a paid speech service rather than GPU time we own.

Both appear in `GET /v1/usage`. Neither can be started from this API.

## When the charge lands

The order below is the same sequence the desktop uses, not a copy of it — one module is the single writer for each product. Every step is where it is for a reason that cost something to learn.

{% stepper %}
{% step %}

### The rate limiter, before anything is read

`generate` costs one token of a bucket free plans do not have. A free key is refused here — a 403 with somewhere to go — rather than after a plan read, a grant, a lane check and a credit transaction that could only ever have ended in 402.
{% endstep %}

{% step %}

### The key's own monthly ceiling, before the wallet

`api_key_charge` reserves against `credit_ceiling`, which is defaulted to your plan's monthly grant when the key is minted. The wallet is per account and the key is not, so without this a leaked key can spend the whole balance.

**It is first because only one of the two is reversible.** Releasing a key's reservation decrements a counter. A wallet charge that has to be undone leaves a permanent pair of ledger rows and burns the idempotency key, so the cheap reversible check runs first and the expensive irreversible one only ever runs for a job that is really going to happen.

A ceiling of `0` means no per-key cap, not "spend nothing". A key that should spend nothing is made by withholding the write scopes instead.
{% endstep %}

{% step %}

### The plan, read from the database

Never from the request. A selection made before a downgrade landed must still be rejected server-side.
{% endstep %}

{% step %}

### The monthly allowance, granted before the balance is checked

So a first-time or just-renewed account is never told it is broke.
{% endstep %}

{% step %}

### Lane admission

`admit_job` takes an advisory lock and counts what is in flight, against a global cap and against the share reserved for API traffic. Refused here, nothing is charged: `429 service_busy`, or `429 too_many_jobs` when it is your own per-account limit.
{% endstep %}

{% step %}

### The charge

`consume_credits`, keyed on `<product>_<idempotency_key>`. Sending the same idempotency key twice never renders twice and never charges twice — the second call answers `200` with `replayed: true` and `credits_charged: 0`, and the key's ceiling reservation is released too, because a replay spent nothing.

Not enough balance is `402 insufficient_credits`, carrying `required` and `balance`.
{% endstep %}

{% step %}

### The row, then the dispatch

The row is the charge's durable home, so a submit that times out after the job really started still has something for a late webhook to adopt.
{% endstep %}
{% endstepper %}

{% hint style="info" %}
**A timeout holds the charge, and that is deliberate.** Credits come back on a *definite* rejection — moderation, a validation error, any 4xx that is not a timeout or a rate limit — because the job then does not exist and never will. A timeout or a 5xx does not mean that: the render may well be running. Those return `502 dispatch_unresolved` with the charge and the `queued` row intact, and a late webhook or the sweep settles it.
{% endhint %}

## What comes back

### A failure refunds in full

You cannot make a GPU die on demand, so a failure is ours. Photo, music and video reverse the whole charge.

**3D is the exception, and it is the honest one.** A 3D job records which stages completed, and their artefacts stay in R2 and stay downloadable. A rigged job that dies in the rig stage leaves you a finished textured mesh, which is a whole product — refunding it in full would be giving away real GPU time. So the charge is split across the stages that were meant to produce it and the completed stages' shares are kept. A 3D failure with **nothing** completed keeps nothing.

The split apportions rather than recomputes: `ceil` of a sum is not the sum of `ceil`s, and here they differ by up to 3 credits. The total is read off the row — the price the job was actually sold at, never a re-run of the current price table — then floored per stage with the leftovers handed to the largest fractional parts, so the shares sum to the charge by construction.

### Partial delivery refunds the part you did not get

**Photo.** The licence requires an output filter, so a refusal is a normal outcome rather than a fault. You keep the base — the container ran — and pay only for images actually handed to you. Every withheld image refunds its per-image credits and nothing else. Refunding the base too would make a filtered request free, and free abuse attempts are the one thing a content filter must not fund.

**Music.** `duration_seconds` is a target; the model stops when a song feels finished, so a 300-second request can return 120 seconds. Only the rate component is refundable — the base paid for a cold start that genuinely happened whatever the length. Ordinary rounding produces nothing: 30.02 seconds against a 30-second request refunds zero, so this does not churn the ledger on every successful job.

### Cancellation differs by product, and that is not an inconsistency

`DELETE /v1/jobs/{id}` stops a job and settles what it was worth. It does not delete the row — the record of what was charged and what came back stays readable.

| Product | What is kept                                                                                                           |
| ------- | ---------------------------------------------------------------------------------------------------------------------- |
| Photo   | Nothing. Full refund.                                                                                                  |
| Music   | Nothing if it never started; otherwise the base plus the fraction burned.                                              |
| Video   | The scenes already rendered.                                                                                           |
| 3D      | Nothing if it never reached `running`; otherwise the completed stages' shares plus the first stage's cold-start share. |
| Chat    | `400 cancel_unsupported` — close the connection instead.                                                               |

**Photo refunds everything** because the whole render is seconds and the pixels are discarded. Billing for a cancelled six-second render would cost more in support than it earns.

**Music keeps nothing on an early cancel** because the WAV is only written at the very end of the pipeline — an early cancel produces no product at all, and charging in full for zero bytes is a support ticket rather than a policy. Once it is running, a full refund would be a cancel-farm: burn 90% of a five-minute render, pay nothing, repeat. The elapsed time comes from the upstream's own `completed_at - started_at`, never from `now()`, because the cancel route and the reconciler can each be the first writer and `now()` would make the refund amount depend on which one arrived first.

**3D keeps the cold-start share** because its cold start reaches $0.231, where music's is $0.054. At that price a cancel-farm is worth someone's time. The share is rounded **down**, which hands the odd credit to you: on `geometry` the cold start is 80% of the whole price, and rounding up would make "cancel a geometry job" mean "no refund, ever".

{% hint style="warning" %}
**Cancelling a video clip submitted from this API refunds the whole charge today.** Video's proration is per scene, and only a chained storyboard has a per-scene price list — which the desktop composer builds. `POST /v1/video` is text-to-video single clips only, so there is nothing to prorate against and the settle falls back to returning everything. Cancel a storyboard and you get back the scenes that were never rendered; cancel an API clip at 90% and you get all your credits back.
{% endhint %}

Cancelling something already finished is `200` with `cancelled: false`, not an error: you wanted it not running, and it is not. 3D can answer `409 cancel_refused` when its service will not stop the container — we say so rather than marking it cancelled while the GPU is still going, which was proven wrong once already by a job that was marked cancelled and refunded and then wrote its model out a minute later.

## GET /v1/usage

It answers the question a bill raises, which is never "what is my balance" — that is on `/v1/me` — but "where did it go". So the shape is spend by product **and** by origin.

{% tabs %}
{% tab title="curl" %}

```bash
curl https://api.auray.ai/v1/usage \
  -H "Authorization: Bearer $AURA_API_KEY"
```

{% endtab %}

{% tab title="JavaScript" %}

```javascript
const res = await fetch("https://api.auray.ai/v1/usage", {
  headers: { Authorization: `Bearer ${process.env.AURA_API_KEY}` },
});
const usage = await res.json();
```

{% endtab %}

{% tab title="Python" %}

```python
import os, requests

usage = requests.get(
    "https://api.auray.ai/v1/usage",
    headers={"Authorization": f"Bearer {os.environ['AURA_API_KEY']}"},
    timeout=30,
).json()
```

{% endtab %}
{% endtabs %}

```json
{
  "object": "usage",
  "period": "2026-08",
  "credits": {
    "balance": 479,
    "monthly_allowance": 600,
    "spent_this_period": 133,
    "refunded_this_period": 12,
    "net_this_period": 121
  },
  "by_product": [
    { "product": "video", "origin": "api", "spent": 74, "refunded": 0,  "net": 74, "jobs": 3 },
    { "product": "chat",  "origin": "app", "spent": 8,  "refunded": 6,  "net": 2,  "jobs": 2 }
  ],
  "keys": [
    {
      "object": "api_key",
      "id": "…", "key_id": "RsLuoyBOjfLN", "name": "nightly",
      "credit_ceiling": 600, "credits_spent_period": 94,
      "last_used_at": "2026-08-25T04:12:09.441Z", "revoked_at": null
    }
  ],
  "rate_limits": { "read": { "capacity": 60, "refill_per_second": 5 } },
  "note": "origin is taken from the debit that paid for a job, because a refund does not record which surface made it. Figures cover the current calendar month."
}
```

**Everything is the current calendar month.** The grant, the ceiling and the reset are all reckoned per calendar month, so anything else here would be a fourth definition of "this period".

**`origin` is `api` or `app`** — your script or somebody's browser. It is taken from the debit that paid for the job rather than from each ledger row, because the refund function does not copy `origin` onto its settlement row. Summing rows as they stand credits every refund back to the browser and makes an API-heavy month look half app-driven; `job_id` is what joins a refund to the debit that knows.

**Grants are excluded from `by_product`** and reported once, as `monthly_allowance`. A grant is a positive ledger row exactly like a refund, and an earlier version of this endpoint summed every positive delta as money returned — it reported a Pro account as 612 refunded against 133 spent, a net of *minus* 479, on a page claiming the account had earned credits by generating.

**`jobs` counts distinct jobs, not ledger rows**, so a job that was refunded counts once rather than twice.

**`product` here is wider than the products you can submit.** It includes `code`, which spends this wallet from the desktop. There is no `POST /v1/code`.

**`credit_ceiling: null` means no per-key cap.** The route reports null rather than the stored `0` so a client cannot read it as a limit of zero. `credits_spent_period` reads 0 for a key whose recorded period is not the current one.

**`rate_limits` is reported at your account's actual age.** An account under 24 hours old gets a fifth of the published limits, and this reflects that rather than promising the full ones.

### One inconsistency worth knowing

`GET /v1/me` reports `balance: null` when the wallet row does not exist at all, and that is the only thing null means there — a failed read is a 500 rather than a number you might act on. `GET /v1/usage` reports `0` for the same account.

Null is "no row". Zero is "a row holding nothing", which is not the same as "you used it all": `grant_plan_credits` inserts the row at 0 before it decides whether to grant anything, so every free account that has opened the desktop reads 0 having never been granted or spent a credit.

## What is not true today

**The API answers nothing.** `API_V1_ENABLED` is unset. Every route above returns `503 api_disabled`.

**Nothing is really charged.** Stripe is in test mode.

**A free key cannot generate, and waiting will not fix it.** `403 plan_has_no_api_generate`, because the `generate` bucket is a capacity of zero rather than an empty one.

**`upscale_2k` is refused on every plan, Ultimate included.** `403 upscale_not_in_plan`. There is no upgrade that accepts it.

**3D runs one API job at a time, worldwide.** Four GPU containers serve the whole platform and the desktop keeps three. That is a slow API rather than a broken one, and you should learn it from this sentence rather than from a 429.

**Video refuses `?wait=` outright** with `400 wait_unsupported`. Its cheapest clip takes longer than the whole request budget, and quietly ignoring the parameter would leave you reading an unfinished job as a finished one. Poll it, or register a webhook.

**Several prices rest on very few measurements.** Photo's per-tier seconds come from a handful of jobs; music's realtime ratio is n=2; 3D's controlled-paint figure is n=5 across a three-fold spread and is the single largest uncertainty in that file. Each of those constants carries a review trigger saying to re-derive it from real traffic. A price that moves will move for jobs submitted after it moved: every route stores what it charged, and cancels, revisions and refunds settle against that stored number rather than re-running the current table.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.auray.ai/documentation/credits-and-plans.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
