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.

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:

ModelInput / MTokOutput / MTokCache read / writeWho
Haiku 5.5 (default), prompts up to 100k tokens$0.10$0.50$0.01 / $0.125everyone
Haiku 5.5, prompts over 100k tokens$0.50$2.50$0.05 / $0.625everyone
Sonnet 5.5$2$10$0.10 / $2.50site owner only
Opus 5.5$4$20$0.20 / $5site 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

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

VariableDefaultMeaning
PHOTO_SPEND_CAP_USD10Hard token-spend cap per member account; 0 = no cap
PHOTO_SPEND_CAP_OWNER_USD50The owner's cap
PHOTO_AGENT_PRICESunsetJSON price overrides per model id
PHOTO_AGENT_MODELhaikuDefault model (key or id) when the account may use it
PHOTO_CATALOG_TTL_DAYS30Lifespan of a member's catalogue items; 0 = never
PHOTO_CATALOG_TTL_OWNER_DAYS90The owner's
PHOTO_CATALOG_SWEEP_S3600Seconds between expiry sweeps; 0 = sweep on read only
ALLOW_UNGATED_ADMINunsetLocally, 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: