> ## Documentation Index
> Fetch the complete documentation index at: https://docs.cycls.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Plans and quotas

> Read the subscription tier from context.user.plan, meter usage on the workspace volume, and verify Apple in-app purchases offline.

This page is for developers adding paid tiers to an agent. It covers reading the
plan claim, enforcing a quota, and accepting an Apple in-app purchase as proof of
entitlement.

**Prerequisites:** an auth provider configured with
[`cycls.Web().auth(...)`](/web/auth), and a volume at `/workspace`.

## Read the plan

`context.user.plan` is whatever your identity provider puts in the JWT claim. The
Cycls-hosted Clerk application emits:

| Value         | Meaning           |
| ------------- | ----------------- |
| `u:free_user` | free individual   |
| `o:free_org`  | free organization |
| `cycls_pass`  | paid subscriber   |

```python theme={null}
if context.user.plan == "cycls_pass":
    yield "Premium features are enabled."
else:
    yield "Upgrade for full access."
    yield {"type": "ui", "action": "open_plan_modal"}
```

`{"type": "ui", "action": "open_plan_modal"}` opens the pricing modal. The client
selects user plans or organization plans based on the active organization.

## Meter usage per user

Store the counter in `cycls.DB`, which writes to the `/workspace` volume. Keying
by month means history accumulates and resets happen without a scheduled job.

```python theme={null}
from datetime import datetime, timezone
import cycls

FREE_MONTHLY_LIMIT = 10


@cycls.agent(image=image, web=web, volumes={"/workspace": cycls.Volume("my-agent")})
async def my_agent(context):
    user = context.user
    exempt = not context.prod                      # local runs are never blocked

    if user.plan == "o:free_org" and not exempt:
        cycls.log("cap_hit", user=user, chat_id=context.chat_id, kind="org_free")
        yield {"type": "callout", "callout": "This workspace needs a paid plan.", "style": "error"}
        yield {"type": "ui", "action": "open_plan_modal"}
        return

    db = cycls.DB(context.workspace)
    month = datetime.now(timezone.utc).strftime("%Y-%m")
    entry = await db.get(f"usage/{month}", {"count": 0})

    if user.plan == "u:free_user" and entry["count"] >= FREE_MONTHLY_LIMIT and not exempt:
        cycls.log("cap_hit", user=user, chat_id=context.chat_id,
                  kind="user_free_monthly", count=entry["count"])
        yield {"type": "callout",
               "callout": f"Free tier limit reached ({FREE_MONTHLY_LIMIT} per month).",
               "style": "warning"}
        yield {"type": "ui", "action": "open_plan_modal"}
        return

    entry["count"] += 1
    await db.put(f"usage/{month}", entry)

    async for ev in llm.run(context=context):
        yield ev
```

Expected behavior: a free user on their eleventh request in a calendar month
sees the warning callout and the pricing modal, and the model is never called.

<Warning>
  `cycls.DB` writes are atomic per key, not transactional across keys. Two requests
  racing on the same counter can both read the same value. For strict enforcement,
  check the counter again after incrementing, or accept a small overshoot.
</Warning>

## Track spend

Set token prices so every turn logs its cost:

```python theme={null}
llm = cycls.LLM().price(input=3, output=15, cache_read=0.30, cache_write=6)
```

Prices are USD per million tokens. Query the result with
[`cycls cost` and `cycls sql`](/ship/observability). Without `.price()`, costs
are logged as zero.

## Apple in-app purchase

Use this when an iOS client sells the subscription and the agent has to trust it
without calling Apple on every request.

```python theme={null}
iap = cycls.AppleIAP(
    bundle_id="com.example.app",
    products={"com.example.app.pro.monthly": "u:pro"},
    namespace="00000000-0000-0000-0000-000000000000",
)

web = cycls.Web().auth(cycls.Clerk()).iap(iap)
```

| Argument    | Required | Meaning                                                           |
| ----------- | -------- | ----------------------------------------------------------------- |
| `bundle_id` | yes      | the app's bundle identifier, checked against the transaction      |
| `products`  | yes      | a set of product ids, or a mapping of product id to plan value    |
| `namespace` | yes      | UUIDv5 namespace the client also uses to derive `appAccountToken` |
| `plan`      | no       | plan value for set-form products, default `u:iap`                 |
| `header`    | no       | request header carrying the JWS, default `x-apple-entitlement`    |
| `root_cert` | no       | override the bundled Apple Root CA G3                             |

How verification works: the iOS client sends its current StoreKit 2 signed
transaction in the header. Cycls validates the certificate chain against the
bundled Apple Root CA G3, checks the product and expiry, and confirms that the
purchase's `appAccountToken` derives from the authenticated user id under your
namespace. A valid entitlement raises `user.plan` for that request only.

The `appAccountToken` binding is what stops a valid receipt from one account
being replayed by another. Both client and server must derive it with the same
UUIDv5 namespace.

## Affiliate tracking

```python theme={null}
web = cycls.Web().affiliate(os.environ["REWARDFUL_KEY"])
```

The key is injected into the page config. The client loads the tracker and
reports conversions at checkout.

## Next

<Card title="Local and remote builds" icon="docker" href="/ship/builds">
  Where a build happens, and which code it contains.
</Card>
