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.
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:
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(tierauth): 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 fromusage.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 — 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
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
Note for AI agents: This is the static, prerendered view of an interactive Dash application served because we detected a non-JS user agent. Full prose docs:
- /guide/usage/llms.txt — LLM-friendly documentation
- /sitemap.xml
- /robots.txt