Vault & content concepts
The mental model behind the content vault — what the index actually is, how OnlyFans writes are staged, and the shared truths content endpoints all reuse.
The vault is TEASE's read model over your OnlyFans content library, plus a thin, carefully
gated write path back onto it. Everything under /api/admin/vault and /api/admin/content
builds on the ideas below — read this first if a how-to page assumes one of them.
The vault index is a mirror, not an upload target
VaultItem rows in our database are a searchable copy of what's already on OnlyFans. You never
upload media to build the index — you trigger a scan,
and the scan reads your already-captured feed (or OFAPI) and writes rows locally. Search,
folders, drops, performance, gaps, dormant, and recommendations are all reads over this mirror —
none of them touch OnlyFans directly.
This is why search is instant and free: GET /vault/search is a LIKE query over a local
table, not a live OnlyFans call.
Deep scan is an engine-only full crawl
The normal scan (POST /vault/scan) re-reads whatever the engine has already captured. A
deep scan (deep=1) is different: it writes a deep_scan.request control file into your
own feed directory so the engine's next poll crawls your entire vault — every cover, every
item — in one slow pass. That crawl only exists on the gateway engine; an account running in
OFAPI mode has no engine to drive it, so deep=1 there is a 400, not a slower scan.
A drop is a folder read as a sellable bundle
A drop groups one folder's items by role — video, photoset, bts (behind-the-scenes),
other — and hands back one ordered bundle_media_ids array: the whole folder, ready to drop
into a single PPV send. Role is classified from the media type plus a BTS keyword match against
the title/tags (English and Russian variants). is_drop is not a mood flag — it's computed:
true only when the folder has at least one video and at least one photoset or BTS item.
Vault management has two levels, and they can never blur
- Level 1 — local organization. Favorite, block, note, and named collections write only to our database. No engine involved, no readiness gate, no queue — click and it's saved.
- Level 2 — OnlyFans folder operations. Creating, renaming, or deleting a folder, and
adding or removing media in one, are real writes to OnlyFans. They go through the engine as an
asynchronous job, and they live in a separate router module (
vault_ops.py, notvault_search.py) specifically so the two worlds can't be confused in the code.
Uploads never send bytes through our backend
Adding a new file to the vault is a three-party handoff: our engine asks OnlyFans for an upload
address, your browser PUTs the file bytes straight to that address, and the engine closes the
upload once the bytes land. Not one byte of your media passes through the TEASE backend — it
only ever sees a name, a size, and a content type going in, and ETags coming back. One
consequence: there's no resume. If the upload is interrupted, you start it again; the engine
cleans up the abandoned multipart parts.
One shared truth for "did the fan buy this"
core.ppv.signal exports exactly three functions — is_purchased, is_sent_unbought,
is_free_send — and every place in Club that needs to know whether a PPV was bought reuses
them: the content library, the payment-content resolver, and the sales poller. A purchase is
never inferred by one team from canPurchase == False alone, because OnlyFans also clears that
flag on a revoked or expired send — is_purchased additionally requires the fan to have
opened the message.
OnlyFans has no payment → media link
There is no field on an OnlyFans transaction that names which vault media it bought. For a
content-less PPV, TEASE resolves the answer the same way the OnlyFans app's own UI does: it
opens the fan's chat live, finds the purchased message nearest the payment in time and price,
and reads its media off that message. The result is then cached into the content_sale table so
the next lookup for the same purchase is instant and never re-reads the chat.
Tags carry provenance, and that provenance is enforced
A ContentTag row always carries a source: manual (a person typed it),
or ai (either the vision tagger over thumbnails, or the text auto-tagger
— both write the same vocabulary so they group together). The API enforces the distinction:
deleting a tag only ever removes a manual one. Asking to delete an AI/vision tag returns an
honest 404, not a silent no-op or a delete of the wrong row.
A pending nonce, not a promise, is what survives a crash
Every OnlyFans-writing endpoint in the vault — folder ops, uploads — answers {"status": "pending", "nonce": "..."} immediately, never {"status": "done"}. The real state lives in the
engine_job journal and is only updated once a background reconciler confirms what actually
happened. This isn't about the engine being slow (the underlying OnlyFans call takes seconds) —
it's about surviving an engine crash, a pod restart, or a dropped connection mid-request: a
synchronous wait inside the HTTP call doesn't survive any of those, and a lost wait invites a
retry that would create a second folder or upload on OnlyFans. Where a retry path exists
(vault folder ops), it re-submits under the same nonce for exactly this reason — see
async engine write gotchas.
What's next
Start with finding content to send, or jump to indexing the vault if nothing is searchable yet.
Source attribution
How Club decides where a paying fan came from — the priority ladder, the four money buckets, and what the reconciliation numbers actually prove.
Dashboards & metrics
The buckets, funnels, and access rules behind every number the Club dashboards show you — and why an empty block is never a guess.