Build OnlyFans Chatter Software
Chatter software is a people problem wearing an API costume. The messaging calls are trivial; what is hard is making sure two chatters never answer the same fan, that the 3am shift knows what the 9pm shift promised, and that six months later you can prove who sent a particular line. This page covers those three — assignment, handover, audit — plus the one piece of API physics that determines how large a desk you can run on a single token. For where the desk sits in the wider operation, see who does what in an agency — chatters, ops owner, creator, and what each one is allowed to touch.
The ceiling nobody plans for
Rate limits are per token per minute, not per user: 60 on Free, 100 on Starter, 500 on Pro. Every chatter in your app shares that budget, and so does every background process you run alongside them.
Work it out before you hire, using your own send-rate numbers. Take a desk where each chatter averages six sends a minute at peak: twelve chatters is 72 requests a minute before you count thread opens, fan lookups and inbox refreshes, and each of those is a request too. On Starter's 100 rpm that desk is throttled during peak; on Pro's 500 it has room. The fix is not only a plan upgrade: serve the inbox from your own store rather than the API, so a chatter scrolling the queue costs you zero requests and only a send or a cold-thread backfill touches the ceiling. The rate limits page has the backoff contract for when you do hit it.
Assignment and locking
The API has no notion of thread ownership. It will happily accept two sends to the same chat from two people a second apart, which is exactly the failure mode fans notice. Ownership lives in your schema:
CREATE TABLE conversation (
account_id BIGINT NOT NULL,
chat_id BIGINT NOT NULL,
assignee_id BIGINT, -- NULL = unassigned, in the pool
lock_expires_at TIMESTAMPTZ, -- soft lock, renewed while the tab is open
status TEXT NOT NULL, -- open | snoozed | escalated | closed
last_message_at TIMESTAMPTZ NOT NULL,
PRIMARY KEY (account_id, chat_id)
);
CREATE INDEX ON conversation (assignee_id, last_message_at DESC);
Make the lock a lease, not a flag. A chatter whose laptop dies holds a boolean lock forever; a lease with a two-minute expiry, renewed by the open tab, releases itself. Claim with a conditional update — UPDATE … WHERE assignee_id IS NULL OR lock_expires_at < now() — so the claim and the check are one atomic statement rather than a read followed by a hopeful write.
Route by creator rather than round-robin where you can. A chatter who works the same three creators every shift remembers what was promised; one who gets a random thread from a roster of twenty does not, and the fan can tell.
Shift handover
The handover is where most desks leak money, because the state that matters is not in any message: what was promised, what was priced, what is pending. Store it as structured fields on the conversation, written by the outgoing chatter and shown to the incoming one before they can take the thread:
- Open promise — "custom clip, agreed Tuesday, 40" with a due date.
- Last price quoted, so the next person does not quote differently.
- Mood flag — warm, negotiating, cooling off, complained.
- Do-not-send — anything the fan has said they do not want.
Backfill the message history itself from GET /accounts/{id}/chats/{chatId}/messages, which pages newest-first with limit (default 20, clamped to 1–100) and an olderThanMessageId cursor rather than an offset. The conversation list behind the queue comes from GET /accounts/{id}/chats with limit and offset. Both are account-scoped, so one desk and one token span the whole roster — the pattern described on the agency automation page.
Keep the inbox live by ingesting message.received deliveries into your own store instead of polling. Deliveries are free, signed, and at-least-once, so dedupe on X-Ofapis-Delivery; the webhooks page has the verification contract. The draft-and-route layer that sits on top of that feed is worked through in automating DMs end to end.
What automation takes off a chatter, and what it does not
Automation here is not a chatter replacement. It is the removal of work a chatter should never have been doing in the first place, and three jobs qualify because none of them needs judgement:
- Greetings and first touches. A
message.receiveddelivery from a fan with no prior thread fires a templated opener within seconds — two credits a send — and the chatter picks the conversation up warm instead of cold. - Segment sends. Tag fans into lists as chatters learn about them, then send a campaign to the list instead of having someone paste the same line 300 times.
- Queue hygiene. Re-open a snoozed thread when the fan replies, flag threads untouched for an hour, drop closed ones out of the view.
What stays human: pricing a custom, reading a fan who is going cold, deciding when to push and when to let a thread rest, and anything a complaint could later be built on. The honest ceiling is roughly the first two exchanges plus the mechanical sends; a desk that automates past that collects refund requests, not revenue.
The audit trail
Log every outbound call as a row at the moment you make it, not derived afterwards from message history. Message history tells you what was said; it does not tell you which of your staff said it.
CREATE TABLE send_log (
id BIGSERIAL PRIMARY KEY,
account_id BIGINT NOT NULL,
chat_id BIGINT NOT NULL,
chatter_id BIGINT NOT NULL,
template_id BIGINT,
text TEXT NOT NULL, -- the rendered text, not the template
price_cents INT NOT NULL DEFAULT 0,
idempotency_key TEXT NOT NULL UNIQUE,
request_id TEXT, -- X-Request-Id, echoed back by the API
http_status INT,
sent_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
request_id is the field that makes a support conversation short: it is echoed on the response, so one id ties your row to a specific gateway call. idempotency_key should be deterministic — a hash of chatter, chat and draft — so that a chatter who double-clicks send gets the original response replayed with Idempotency-Replayed: true rather than sending twice, and a request fired while the first is still in flight gets 409 IDEMPOTENCY_CONFLICT instead of a duplicate DM.
Store the rendered text, not just the template_id. Templates get edited; a dispute six months later is about the exact words that went out.
Per-chatter attribution, honestly
You can attribute sends exactly: every row above has a chatter_id. Attributing revenue is harder and worth being precise about, because there is no aggregate earnings or statistics endpoint to reconcile against — as the statistics page explains, revenue has to be reconstructed from the monetary fields on messages and notifications. That means a nightly job that re-reads recently touched threads, matches the paid messages back to your send_log rows by message id, and credits the chatter who sent them.
Two caveats to put in front of whoever reads the dashboard. That reconciliation costs a credit per page read, so scope it to threads touched in the last 48 hours rather than the whole inbox. And last-touch attribution flatters closers at the expense of whoever warmed the fan up over three shifts — if you pay commission on it, expect the incentive to show up in behaviour within a month.
Why per-call billing suits a desk
Chatter tooling is traditionally sold per seat, which taxes you for staffing the night shift. Billing here is per call regardless of how many people are logged in — one credit for most routes, two for an account-scoped send. Rejections at the gate (401, 402, 403, 429) never reach metering, and 5xx, 424 (the creator's OnlyFans session expired) and 409 are refunded — but a 400, 404 or 422 is charged like any other call. A desk's bill therefore tracks messages handled rather than headcount. A mass send debits one credit per recipient, so a 3,000-fan blast is 3,000 credits, a tenth of a Starter month, and a careless campaign can consume what the desk needs for its shifts; see pricing for the allowances. The sizing test worth running before you commit: one creator and two chatters for a week with your own send log on, which gives you the real requests-per-send multiplier and the real credit cost of a handled conversation — the two numbers that pick the plan.
FAQ
How do I stop two chatters answering the same fan?
Hold ownership in your own database as a short lease — an assignee_id plus a lock_expires_at renewed by the open tab — and claim it with a single conditional UPDATE. The API enforces nothing about ownership, so a boolean lock without an expiry will strand threads whenever a browser crashes.
What has to survive a shift handover?
The things no message contains: open promises with due dates, the last price quoted, the fan's mood, and anything they have asked not to receive. Store these as fields on the conversation and show them before the incoming chatter can take the thread, or the next shift re-quotes a price and loses the sale.
Can I prove which chatter sent a given message?
Only if you log it yourself at send time. Record chatter_id, the rendered text, the idempotency key and the echoed X-Request-Id on every outbound call; message history read back later shows the creator account as the sender and nothing about your staff.
Will this replace my chatters?
No, and anyone selling that is selling you refund requests. What it removes is the mechanical part of a shift: the templated opener on a first inbound DM, the same line pasted to 300 fans, the threads nobody has touched in an hour. Pricing a custom, reading a fan going cold and handling a complaint stay with the person — the API has no opinion about any of them.
Does a bigger team cost more?
Not in licensing — billing is per call, not per seat, so staffing the night shift costs nothing extra. It is not only successful calls, though: a 400, 404 or 422 is charged, while gate rejections (401, 402, 403, 429) are free and 5xx, 424 and 409 are refunded. The real constraint is the shared per-token rate limit of 60, 100 or 500 requests a minute, so a large desk needs a higher plan and an inbox served from your own store rather than from live API reads.
How do I attribute revenue to a chatter?
Reconcile after the fact: re-read recently active threads, match paid messages back to your send log by message id, and credit the sender. There is no earnings endpoint to query directly, so budget a credit per page of that nightly read and scope it to threads touched in the last two days.