# Charming pricing & limits

> Free for the first app — no signup until you decide to keep it. Anonymous apps are kept for 7 days; sign in to claim and keep them indefinitely. There is no enforced app cap per account today (fair-use); free-tier rate limits and per-app size caps apply to every account. Source of truth for the numbers below is `apps/server/src/openapi/spec.ts` plus the per-route 429 caps in `apps/server/src/apps/route-handlers/*` and the token TTL constants in `apps/server/src/auth/*`. The machine-readable spec is at <https://charm.ing/.well-known/openapi.json>.

## Offer

Charming is free to try. An anonymous `POST /app` mints the app row plus a one-time `bld_app_*` token; the agent keeps building against that token, the human user decides later whether to sign in and claim. There is no signup wall, no metering on the create call, and no card on file before claim. After claim, the app belongs to the user permanently — the anonymous-app TTL stops applying and the `bld_app_*` token is revoked in favour of the user's `bld_user_*` (see [auth.md](https://usecharming.com/auth.md)).

## Tiers at a glance

| Tier | Signup required | App persistence | Token used | Claim flow |
| --- | --- | --- | --- | --- |
| Anonymous | No | 7-day TTL from creation | `bld_app_*` (one app) | `POST /app/{id}/claim` transfers ownership |
| Claimed (signed-in user) | Yes | Indefinite | `bld_user_*` (account-wide) | n/a — already claimed |

Anonymous apps work the same as claimed apps for create/update/I/O. The only differences are:

- the `expiresAt` field is set on anonymous-create response and the row is reaped 7 days after creation if it is never claimed
- the `bld_app_*` token authorises mutations on that one app only (not account-wide)
- after claim, the `bld_app_*` returns 401 with `reason: "token_revoked_post_claim"` and a self-contained `recovery` block that points at the device-pairing flow — see [auth.md](https://usecharming.com/auth.md#errors)

## Per-account limits

| Resource                      | Limit                            | Source                    |
| ----------------------------- | -------------------------------- | ------------------------- |
| Apps per user                 | No enforced cap today (fair-use) | n/a                       |
| `list_apps` default page size | 50                               | `LIST_APPS_DEFAULT_LIMIT` |
| `list_apps` max page size     | 100                              | `LIST_APPS_MAX_LIMIT`     |

Listing only returns the caller's apps (owned + collaborator); paging uses an opaque `cursor`.

## Per-app limits

| Resource | Cap | Error kind on breach |
| --- | --- | --- |
| `module` source | 256 KiB | `module_too_large` (413) |
| `ui` source | 256 KiB | `ui_too_large` (413) |
| `description` | 500 JS chars | `description_too_long` (400) |
| `starter_prompt` | 2000 JS chars | `invalid_input` (400) |
| Feedback `text` body | 16 KiB chars | `text_too_large` (413) |
| Feedback `structuredData` (serialized) | 64 KiB | `structured_data_too_large` (413) |
| Feedback wire body | 96 KiB | `payload_too_large` (413) |
| Edits batch (`PATCH /app/{id}/source`) | per-string + per-batch cap | `edits_too_large` (413) |
| Diag report body (`POST /app/{id}/diag/report`) | 16 KiB | `payload_too_large` (413) |
| Load-error report body | 2 KiB | `payload_too_large` (413) |
| Per-app asset size | 10 MiB | `asset_too_large` (413) |
| Per-app asset count | 50 | `asset_count_exceeded` (413) |
| Per-app asset total | 100 MiB | `asset_quota_exceeded` (413) |
| Per-app secret value | 8 KiB | `secret_too_large` (400) |
| Per-app secret name uniqueness | one row per `(scope, manifestId, name)` | `secret_already_exists` (409) |
| Per-app diag ring buffer | 50 entries (oldest rolls off) | n/a — in-memory ring |
| Durable activity events | 90-day retention, kind-capped | n/a |

## Rate limits

Token-bucket limiters, in-memory, per process. All windows are 60 seconds.

| Route family | Per `(app, IP)` | Per app | Per IP | Per user |
| --- | --- | --- | --- | --- |
| `POST /app/{id}/diag/report` | 30/min | 200/min | — | — |
| `POST /app/{id}/load-error/report` | 30/min | 200/min | — | — |
| `POST /app/{id}/feedback` | 30/min | 200/min | — | — |
| `POST /app/{id}/assets` / `DELETE /app/{id}/assets/{key}` | throttled per app | — | — | — |
| `POST /api/pair/start` | — | — | 30/min | — |
| `POST /api/pair/poll` | — | — | 60/min | — |
| `POST /api/pair/approve` | — | — | — | 20/min |

A breach returns 429 with `error.kind: "rate_limited"` (or `"too_many_requests"` for asset writes).

### Rate-limit response headers

Every rate-limited route emits the draft-IETF `RateLimit-*` headers (plus their legacy `X-RateLimit-*` aliases for older agent tooling) on both success and 429 responses so a client can project headroom without busy-polling:

| Header | Value | When |
| --- | --- | --- |
| `RateLimit-Limit` | Capacity of the most-restrictive bucket gating this request (e.g. `30`). | every response |
| `RateLimit-Remaining` | Floored whole tokens left in that bucket. `0` on the response that exhausts it. | every response |
| `RateLimit-Reset` | Integer seconds until the next whole token. `0` while remaining > 0. | every response |
| `X-RateLimit-Limit` / `X-RateLimit-Remaining` / `X-RateLimit-Reset` | Legacy aliases — same values as the IETF triplet. | every response |
| `Retry-After` | Integer seconds the client should wait before retrying. Equals `RateLimit-Reset` on every 429. | 429 only |

The numbers reflect the most-restrictive bucket among all that gate the request (e.g. `(app, IP)` vs `(app)` for diag/feedback/load-error). The `Retry-After` value lets generic HTTP clients back off without parsing the body.

## Token TTLs

| Token | Default TTL | Notes |
| --- | --- | --- |
| `bld_app_*` (anonymous create) | Until claimed or app reaped (7 days unclaimed) | Authorises one app; revoked on claim |
| `bld_user_*` (`POST /api/token`) | 7 days | Personal access token; rotate by minting a new one |
| `bld_user_*` (paired via `POST /api/pair/start`) | 30 days | Issued when a user approves a device-pairing grant |
| `bld_render_*` (server-minted at render) | 7 days (rotates on re-render; previous hash kept live for a ~15-minute grace window) | Bound to one app id; not user-mintable |
| Device-pairing grant (`device_code` + `user_code`) | 10 minutes (hard cap 60 minutes with active polling) | RFC 8628-shaped; `user_code` displayed to human |

`bld_user_*` token revocation is immediate (`DELETE /api/token/{id}`). The `/account/agents` UI is the user-facing inventory; programmatic listing is `GET /api/token`. See [auth.md](https://usecharming.com/auth.md) for the full revocation surface.

## What is NOT metered today

- App create / update calls (no per-minute write limit on `POST /app` or `PUT /app/{id}`)
- App reads (`GET /app/{id}`, `GET /app/{id}/source`, `GET /app/{id}/describe`)
- MCP `query_app` / `mutate_app` calls — those are the runtime operating on the app's own handler

The route-layer rate limits cover ingress paths that an anonymous caller can hit (diag/feedback/asset writes, pairing) — i.e. surfaces that exist precisely because the iframe sandbox cannot carry credentials. Mutating an app through the authenticated PUT/PATCH path is gated by the bearer token, not a per-minute counter.

## When numbers change

These caps are encoded as exported constants in the server (`MODULE_MAX_BYTES`, `LIST_APPS_DEFAULT_LIMIT`, `ANON_APP_TTL_MS`, `TOKEN_TTL_MS`, `PAIR_TTL_MS`, `PAIRED_TOKEN_TTL_MS`, `FEEDBACK_TEXT_MAX_LEN`, `FEEDBACK_STRUCTURED_DATA_MAX_LEN`, `ASSET_MAX_BYTES`, `ASSET_COUNT_MAX`, `ASSET_TOTAL_BYTES_MAX`, and the per-route `capacity` / `windowMs` arguments to `createRateLimiter`). The OpenAPI spec re-derives them; this doc mirrors the spec verbatim. If a number moves, the spec moves too — fetch <https://charm.ing/.well-known/openapi.json> for the live value.
