# Usage, limits & costs

> The guard rails on an open Photo Slop — a $10 token cap per account, owner-only Opus, 30/90-day catalogue lifespans — and the two dashboards that show who spent and kept what.

**Site index:** [https://photoslop.2plot.dev/llms.txt](https://photoslop.2plot.dev/llms.txt) — every page on this site, as Markdown.  
**Network index:** [https://2plot.dev/llms.txt](https://2plot.dev/llms.txt) — The 2plot network; start here to discover sibling sites.  
**Sibling sites:** 14 more in The 2plot network — listed in the site index above.  
**Sitemap:** https://photoslop.2plot.dev/sitemap.xml  


---



## Why there are rails

Anyone with an account on the 2plot network can open photoslop.2plot.dev, make a canvas, drop images, try the pixel tools and ask the agent to edit for them. The agent calls a paid model and the catalogue fills a paid bucket, so an open door needs rails that are **per account, visible to the account, and summed for the owner**. That is what `lib/photo/ledger.py` keeps and what `/usage` and `/admin/usage` show.

## Token spend and the $10 cap

Every call the agent makes is priced the moment the response arrives, from the public per-million-token table for the model that answered, and written against the signed-in account:

| Model | Input / MTok | Output / MTok | Cache read / write | Who |
|---|---|---|---|---|
| Haiku 5.5 (default), prompts up to 100k tokens | $0.10 | $0.50 | $0.01 / $0.125 | everyone |
| Haiku 5.5, prompts over 100k tokens | $0.50 | $2.50 | $0.05 / $0.625 | everyone |
| Sonnet 5.5 | $2 | $10 | $0.10 / $2.50 | **site owner only** |
| Opus 5.5 | $4 | $20 | $0.20 / $5 | **site owner only** |

Each account has a **hard cap**: `PHOTO_SPEND_CAP_USD`, $10 by default; the owner's own cap is `PHOTO_SPEND_CAP_OWNER_USD` ($50). The cap is checked **before** every model call, so an account at its cap gets a one-line message in the Agent tab and no request is made. The call that crosses the cap is the last one, which is why an account can end a few cents over; the Agent tab prints what each request cost and the running total under the prompt box — "Used $x of $y · last request $z", with a two-tone bar whose striped end is the last request's share — and the finished line of every conversation names the request's cost.

Haiku 5.5 is priced by prompt length: a request whose prompt (input plus cache reads and writes) is over 100,000 tokens pays the higher rates for the whole request, and the ledger prices each response on its own the same way. Prices are constants in the ledger because a price is part of what a user is promised. `PHOTO_AGENT_PRICES` (a JSON object `{"<model id>": {"input": …, "output": …, "cache_write": …, "cache_read": …}}`) overrides them per deployment; a model id the table does not know is priced at Opus rates, the conservative guess.

The agent needs an account to count against, so it is disabled for signed-out visitors (the tab says so). The model menu is built from what the request's account may use, and the choice is validated again on the server: a member asking for Opus is refused before any request, whatever the browser sent.

## Catalogue lifespans

Everything saved to a catalogue — projects, images, the sources behind them — carries an `expires` date: **30 days** after it was saved for a member, **90** for the owner (`PHOTO_CATALOG_TTL_DAYS` / `PHOTO_CATALOG_TTL_OWNER_DAYS`; `0` switches lifespans off). Each card says when it goes; the catalogue header says so too. Expired items are removed, files included, the next time the catalogue is read, by the hourly sweep `run.py` starts (`PHOTO_CATALOG_SWEEP_S`, `0` disables it), and by hand:

```bash
python3 scripts/catalog_purge.py --dry-run     # what would go, per account
python3 scripts/catalog_purge.py               # delete it
```

Rows saved before lifespans existed are given the member lifespan from their creation date on the first sweep, so an old catalogue ages out the same way rather than vanishing at once.

## Storage accounting

The bytes an account keeps are read from its catalogue (split into projects, images and sources). Writes and reads the app makes for an account are counted as Cloudflare R2 bills them — Class A operations for puts, Class B for gets, deletes free — and priced at list: $0.015 per GB-month, $4.50 per million Class A, $0.36 per million Class B, assuming the bytes stay the whole month. The free tier (10 GB-month, 1M A, 10M B per month) belongs to the bucket, not to a user, so it is deducted only in the owner's total.

## The two dashboards

- **`/usage`** (tier `auth`): the signed-in account's own board — spend against the cap with a bar, requests and tokens, spend per day and per model, storage by kind with the monthly estimate, the recent agent sessions — one bill per conversation that opens to the individual model calls — the rules in force and the price table. Nothing on it belongs to anyone else: every figure comes from `usage.account_report(owner)` with the owner taken from the request.
- **`/admin/usage`** (owner only, the control board's gate): every account in one table, named by the email Clerk gave (the key-safe id underneath) — tier, spent, cap, left, requests, calls, tokens, canvases and sources, bytes, writes and reads, storage $/month, next expiry, last seen — with spend per day across accounts, spend per model, spend and storage per account, the bucket's list and after-free-tier cost, and everyone's recent sessions, one bill each, opening to the calls. Fails closed without Clerk, like `/admin/traffic`.

The charts are [dash-mui-charts](https://muicharts.2plot.dev) — a bar of spend per day, donuts of spend per model and storage per kind or account — mounted once with no refresh interval.

## The API

```text
GET /photo/api/usage          the signed-in account's report (401 when signed out)
GET /photo/api/usage?all=1    the owner's report over every account (403 for anyone else)
```

Both return what the pages render: `account` (spend, cap, remaining, tokens, per-model totals, storage operations), `daily`, `events`, `storage` (the catalogue summary and the R2 estimate), `models`, `prices`, `lifespanDays`; the fleet report adds `accounts`, `totals`, `caps`, `lifespans` and the backend description.

## Where the ledger lives

One JSON object, kept in the process and written through to the asset backend under `ledger/ledger.json` — a file on disk locally, an object in the bucket in production — so it survives a deploy without a database. The key is never served by `/photo/files`. Like the catalogue index and the agent's run table, it assumes one gunicorn worker; a multi-worker deployment needs a shared store.

## Environment

| Variable | Default | Meaning |
|---|---|---|
| `PHOTO_SPEND_CAP_USD` | `10` | Hard token-spend cap per member account; `0` = no cap |
| `PHOTO_SPEND_CAP_OWNER_USD` | `50` | The owner's cap |
| `PHOTO_AGENT_PRICES` | unset | JSON price overrides per model id |
| `PHOTO_AGENT_MODEL` | `haiku` | Default model (key or id) when the account may use it |
| `PHOTO_CATALOG_TTL_DAYS` | `30` | Lifespan of a member's catalogue items; `0` = never |
| `PHOTO_CATALOG_TTL_OWNER_DAYS` | `90` | The owner's |
| `PHOTO_CATALOG_SWEEP_S` | `3600` | Seconds between expiry sweeps; `0` = sweep on read only |
| `ALLOW_UNGATED_ADMIN` | unset | Locally, with Clerk off: `1` makes the dev stand-in the owner |


---

*Source: /guide/usage*
